ci: enforce code formatting checks for backend and frontend (#1536)

This commit is contained in:
greatmengqi 2026-03-29 15:34:38 +08:00 committed by GitHub
parent 06a623f9c8
commit 084dc7e748
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
105 changed files with 8253 additions and 7369 deletions

View File

@ -53,6 +53,11 @@ jobs:
cd frontend cd frontend
pnpm install --frozen-lockfile pnpm install --frozen-lockfile
- name: Check frontend formatting
run: |
cd frontend
pnpm format
- name: Run frontend linting - name: Run frontend linting
run: | run: |
cd frontend cd frontend

View File

@ -12,6 +12,7 @@ test:
lint: lint:
uvx ruff check . uvx ruff check .
uvx ruff format --check .
format: format:
uvx ruff check . --fix && uvx ruff format . uvx ruff check . --fix && uvx ruff format .

View File

@ -66,14 +66,9 @@ def _normalize_custom_agent_name(raw_value: str) -> str:
"""Normalize legacy channel assistant IDs into valid custom agent names.""" """Normalize legacy channel assistant IDs into valid custom agent names."""
normalized = raw_value.strip().lower().replace("_", "-") normalized = raw_value.strip().lower().replace("_", "-")
if not normalized: if not normalized:
raise InvalidChannelSessionConfigError( raise InvalidChannelSessionConfigError("Channel session assistant_id is empty. Use 'lead_agent' or a valid custom agent name.")
"Channel session assistant_id is empty. Use 'lead_agent' or a valid custom agent name."
)
if not CUSTOM_AGENT_NAME_PATTERN.fullmatch(normalized): if not CUSTOM_AGENT_NAME_PATTERN.fullmatch(normalized):
raise InvalidChannelSessionConfigError( raise InvalidChannelSessionConfigError(f"Invalid channel session assistant_id {raw_value!r}. Use 'lead_agent' or a custom agent name containing only letters, digits, and hyphens.")
f"Invalid channel session assistant_id {raw_value!r}. "
"Use 'lead_agent' or a custom agent name containing only letters, digits, and hyphens."
)
return normalized return normalized

View File

@ -48,9 +48,7 @@ def _make_file_sandbox_writable(file_path: os.PathLike[str] | str) -> None:
logger.warning("Skipping sandbox chmod for symlinked upload path: %s", file_path) logger.warning("Skipping sandbox chmod for symlinked upload path: %s", file_path)
return return
writable_mode = ( writable_mode = stat.S_IMODE(file_stat.st_mode) | stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH
stat.S_IMODE(file_stat.st_mode) | stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH
)
chmod_kwargs = {"follow_symlinks": False} if os.chmod in os.supports_follow_symlinks else {} chmod_kwargs = {"follow_symlinks": False} if os.chmod in os.supports_follow_symlinks else {}
os.chmod(file_path, writable_mode, **chmod_kwargs) os.chmod(file_path, writable_mode, **chmod_kwargs)

View File

@ -71,9 +71,7 @@ class FileMemoryStorage(MemoryStorage):
if not agent_name: if not agent_name:
raise ValueError("Agent name must be a non-empty string.") raise ValueError("Agent name must be a non-empty string.")
if not AGENT_NAME_PATTERN.match(agent_name): if not AGENT_NAME_PATTERN.match(agent_name):
raise ValueError( raise ValueError(f"Invalid agent name {agent_name!r}: names must match {AGENT_NAME_PATTERN.pattern}")
f"Invalid agent name {agent_name!r}: names must match {AGENT_NAME_PATTERN.pattern}"
)
def _get_memory_file_path(self, agent_name: str | None = None) -> Path: def _get_memory_file_path(self, agent_name: str | None = None) -> Path:
"""Get the path to the memory file.""" """Get the path to the memory file."""
@ -180,18 +178,15 @@ def get_memory_storage() -> MemoryStorage:
try: try:
module_path, class_name = storage_class_path.rsplit(".", 1) module_path, class_name = storage_class_path.rsplit(".", 1)
import importlib import importlib
module = importlib.import_module(module_path) module = importlib.import_module(module_path)
storage_class = getattr(module, class_name) storage_class = getattr(module, class_name)
# Validate that the configured storage is a MemoryStorage implementation # Validate that the configured storage is a MemoryStorage implementation
if not isinstance(storage_class, type): if not isinstance(storage_class, type):
raise TypeError( raise TypeError(f"Configured memory storage '{storage_class_path}' is not a class: {storage_class!r}")
f"Configured memory storage '{storage_class_path}' is not a class: {storage_class!r}"
)
if not issubclass(storage_class, MemoryStorage): if not issubclass(storage_class, MemoryStorage):
raise TypeError( raise TypeError(f"Configured memory storage '{storage_class_path}' is not a subclass of MemoryStorage")
f"Configured memory storage '{storage_class_path}' is not a subclass of MemoryStorage"
)
_storage_instance = storage_class() _storage_instance = storage_class()
except Exception as e: except Exception as e:

View File

@ -27,10 +27,12 @@ def _save_memory_to_file(memory_data: dict[str, Any], agent_name: str | None = N
"""Backward-compatible wrapper around the configured memory storage save path.""" """Backward-compatible wrapper around the configured memory storage save path."""
return get_memory_storage().save(memory_data, agent_name) return get_memory_storage().save(memory_data, agent_name)
def get_memory_data(agent_name: str | None = None) -> dict[str, Any]: def get_memory_data(agent_name: str | None = None) -> dict[str, Any]:
"""Get the current memory data via storage provider.""" """Get the current memory data via storage provider."""
return get_memory_storage().load(agent_name) return get_memory_storage().load(agent_name)
def reload_memory_data(agent_name: str | None = None) -> dict[str, Any]: def reload_memory_data(agent_name: str | None = None) -> dict[str, Any]:
"""Reload memory data via storage provider.""" """Reload memory data via storage provider."""
return get_memory_storage().reload(agent_name) return get_memory_storage().reload(agent_name)

View File

@ -162,10 +162,7 @@ class ClaudeChatModel(ChatAnthropic):
system = payload.get("system") system = payload.get("system")
if isinstance(system, list): if isinstance(system, list):
# Remove any existing billing blocks, then insert a single one at index 0. # Remove any existing billing blocks, then insert a single one at index 0.
filtered = [ filtered = [b for b in system if not (isinstance(b, dict) and OAUTH_BILLING_HEADER in b.get("text", ""))]
b for b in system
if not (isinstance(b, dict) and OAUTH_BILLING_HEADER in b.get("text", ""))
]
payload["system"] = [billing_block] + filtered payload["system"] = [billing_block] + filtered
elif isinstance(system, str): elif isinstance(system, str):
if OAUTH_BILLING_HEADER in system: if OAUTH_BILLING_HEADER in system:
@ -183,11 +180,13 @@ class ClaudeChatModel(ChatAnthropic):
hostname = socket.gethostname() hostname = socket.gethostname()
device_id = hashlib.sha256(f"deerflow-{hostname}".encode()).hexdigest() device_id = hashlib.sha256(f"deerflow-{hostname}".encode()).hexdigest()
session_id = str(uuid.uuid4()) session_id = str(uuid.uuid4())
payload["metadata"]["user_id"] = json.dumps({ payload["metadata"]["user_id"] = json.dumps(
"device_id": device_id, {
"account_uuid": "deerflow", "device_id": device_id,
"session_id": session_id, "account_uuid": "deerflow",
}) "session_id": session_id,
}
)
def _apply_prompt_caching(self, payload: dict) -> None: def _apply_prompt_caching(self, payload: dict) -> None:
"""Apply ephemeral cache_control to system and recent messages.""" """Apply ephemeral cache_control to system and recent messages."""

View File

@ -84,9 +84,7 @@ class PatchedChatOpenAI(ChatOpenAI):
else: else:
# Fallback: match assistant-role entries positionally against AIMessages. # Fallback: match assistant-role entries positionally against AIMessages.
ai_messages = [m for m in original_messages if isinstance(m, AIMessage)] ai_messages = [m for m in original_messages if isinstance(m, AIMessage)]
assistant_payloads = [ assistant_payloads = [(i, m) for i, m in enumerate(payload_messages) if m.get("role") == "assistant"]
(i, m) for i, m in enumerate(payload_messages) if m.get("role") == "assistant"
]
for (_, payload_msg), ai_msg in zip(assistant_payloads, ai_messages): for (_, payload_msg), ai_msg in zip(assistant_payloads, ai_messages):
_restore_tool_call_signatures(payload_msg, ai_msg) _restore_tool_call_signatures(payload_msg, ai_msg)

View File

@ -100,7 +100,7 @@ def _resolve_skills_path(path: str) -> str:
if path == skills_container: if path == skills_container:
return skills_host return skills_host
relative = path[len(skills_container):].lstrip("/") relative = path[len(skills_container) :].lstrip("/")
return _join_path_preserving_style(skills_host, relative) return _join_path_preserving_style(skills_host, relative)

View File

@ -197,6 +197,7 @@ async def task_tool(
writer({"type": "task_timed_out", "task_id": task_id}) writer({"type": "task_timed_out", "task_id": task_id})
return f"Task polling timed out after {timeout_minutes} minutes. This may indicate the background task is stuck. Status: {result.status.value}" return f"Task polling timed out after {timeout_minutes} minutes. This may indicate the background task is stuck. Status: {result.status.value}"
except asyncio.CancelledError: except asyncio.CancelledError:
async def cleanup_when_done() -> None: async def cleanup_when_done() -> None:
max_cleanup_polls = max_poll_count max_cleanup_polls = max_poll_count
cleanup_poll_count = 0 cleanup_poll_count = 0
@ -211,9 +212,7 @@ async def task_tool(
return return
if cleanup_poll_count > max_cleanup_polls: if cleanup_poll_count > max_cleanup_polls:
logger.warning( logger.warning(f"[trace={trace_id}] Deferred cleanup for task {task_id} timed out after {cleanup_poll_count} polls")
f"[trace={trace_id}] Deferred cleanup for task {task_id} timed out after {cleanup_poll_count} polls"
)
return return
await asyncio.sleep(5) await asyncio.sleep(5)

View File

@ -118,9 +118,7 @@ def _regex_score(pattern: str, entry: DeferredToolEntry) -> int:
# loop.run_in_executor, Python copies the current context to the worker thread, # loop.run_in_executor, Python copies the current context to the worker thread,
# so the ContextVar value is correctly inherited there too. # so the ContextVar value is correctly inherited there too.
_registry_var: contextvars.ContextVar[DeferredToolRegistry | None] = contextvars.ContextVar( _registry_var: contextvars.ContextVar[DeferredToolRegistry | None] = contextvars.ContextVar("deferred_tool_registry", default=None)
"deferred_tool_registry", default=None
)
def get_deferred_registry() -> DeferredToolRegistry | None: def get_deferred_registry() -> DeferredToolRegistry | None:

View File

@ -600,10 +600,7 @@ class TestChannelManager:
await manager.stop() await manager.stop()
mock_client.runs.wait.assert_not_called() mock_client.runs.wait.assert_not_called()
assert outbound_received[0].text == ( assert outbound_received[0].text == ("Invalid channel session assistant_id 'bad agent!'. Use 'lead_agent' or a custom agent name containing only letters, digits, and hyphens.")
"Invalid channel session assistant_id 'bad agent!'. "
"Use 'lead_agent' or a custom agent name containing only letters, digits, and hyphens."
)
_run(go()) _run(go())

View File

@ -56,10 +56,7 @@ def test_billing_not_duplicated_on_second_call(model):
payload = {"system": [{"type": "text", "text": "prompt"}]} payload = {"system": [{"type": "text", "text": "prompt"}]}
model._apply_oauth_billing(payload) model._apply_oauth_billing(payload)
model._apply_oauth_billing(payload) model._apply_oauth_billing(payload)
billing_count = sum( billing_count = sum(1 for b in payload["system"] if isinstance(b, dict) and OAUTH_BILLING_HEADER in b.get("text", ""))
1 for b in payload["system"]
if isinstance(b, dict) and OAUTH_BILLING_HEADER in b.get("text", "")
)
assert billing_count == 1 assert billing_count == 1

View File

@ -65,14 +65,7 @@ class TestClientInit:
def test_custom_params(self, mock_app_config): def test_custom_params(self, mock_app_config):
mock_middleware = MagicMock() mock_middleware = MagicMock()
with patch("deerflow.client.get_app_config", return_value=mock_app_config): with patch("deerflow.client.get_app_config", return_value=mock_app_config):
c = DeerFlowClient( c = DeerFlowClient(model_name="gpt-4", thinking_enabled=False, subagent_enabled=True, plan_mode=True, agent_name="test-agent", middlewares=[mock_middleware])
model_name="gpt-4",
thinking_enabled=False,
subagent_enabled=True,
plan_mode=True,
agent_name="test-agent",
middlewares=[mock_middleware]
)
assert c._model_name == "gpt-4" assert c._model_name == "gpt-4"
assert c._thinking_enabled is False assert c._thinking_enabled is False
assert c._subagent_enabled is True assert c._subagent_enabled is True

View File

@ -132,18 +132,13 @@ def test_build_middlewares_uses_resolved_model_name_for_vision(monkeypatch):
monkeypatch.setattr(lead_agent_module, "_create_summarization_middleware", lambda: None) monkeypatch.setattr(lead_agent_module, "_create_summarization_middleware", lambda: None)
monkeypatch.setattr(lead_agent_module, "_create_todo_list_middleware", lambda is_plan_mode: None) monkeypatch.setattr(lead_agent_module, "_create_todo_list_middleware", lambda is_plan_mode: None)
middlewares = lead_agent_module._build_middlewares( middlewares = lead_agent_module._build_middlewares({"configurable": {"model_name": "stale-model", "is_plan_mode": False, "subagent_enabled": False}}, model_name="vision-model", custom_middlewares=[MagicMock()])
{"configurable": {"model_name": "stale-model", "is_plan_mode": False, "subagent_enabled": False}},
model_name="vision-model",
custom_middlewares=[MagicMock()]
)
assert any(isinstance(m, lead_agent_module.ViewImageMiddleware) for m in middlewares) assert any(isinstance(m, lead_agent_module.ViewImageMiddleware) for m in middlewares)
# verify the custom middleware is injected correctly # verify the custom middleware is injected correctly
assert len(middlewares) > 0 and isinstance(middlewares[-2], MagicMock) assert len(middlewares) > 0 and isinstance(middlewares[-2], MagicMock)
def test_create_summarization_middleware_uses_configured_model_alias(monkeypatch): def test_create_summarization_middleware_uses_configured_model_alias(monkeypatch):
monkeypatch.setattr( monkeypatch.setattr(
lead_agent_module, lead_agent_module,

View File

@ -33,6 +33,7 @@ class TestMemoryStorageInterface:
def test_abstract_methods(self): def test_abstract_methods(self):
"""Should raise TypeError when trying to instantiate abstract class.""" """Should raise TypeError when trying to instantiate abstract class."""
class TestStorage(MemoryStorage): class TestStorage(MemoryStorage):
pass pass
@ -45,6 +46,7 @@ class TestFileMemoryStorage:
def test_get_memory_file_path_global(self, tmp_path): def test_get_memory_file_path_global(self, tmp_path):
"""Should return global memory file path when agent_name is None.""" """Should return global memory file path when agent_name is None."""
def mock_get_paths(): def mock_get_paths():
mock_paths = MagicMock() mock_paths = MagicMock()
mock_paths.memory_file = tmp_path / "memory.json" mock_paths.memory_file = tmp_path / "memory.json"
@ -58,6 +60,7 @@ class TestFileMemoryStorage:
def test_get_memory_file_path_agent(self, tmp_path): def test_get_memory_file_path_agent(self, tmp_path):
"""Should return per-agent memory file path when agent_name is provided.""" """Should return per-agent memory file path when agent_name is provided."""
def mock_get_paths(): def mock_get_paths():
mock_paths = MagicMock() mock_paths = MagicMock()
mock_paths.agent_memory_file.return_value = tmp_path / "agents" / "test-agent" / "memory.json" mock_paths.agent_memory_file.return_value = tmp_path / "agents" / "test-agent" / "memory.json"
@ -68,9 +71,7 @@ class TestFileMemoryStorage:
path = storage._get_memory_file_path("test-agent") path = storage._get_memory_file_path("test-agent")
assert path == tmp_path / "agents" / "test-agent" / "memory.json" assert path == tmp_path / "agents" / "test-agent" / "memory.json"
@pytest.mark.parametrize( @pytest.mark.parametrize("invalid_name", ["", "../etc/passwd", "agent/name", "agent\\name", "agent name", "agent@123", "agent_name"])
"invalid_name", ["", "../etc/passwd", "agent/name", "agent\\name", "agent name", "agent@123", "agent_name"]
)
def test_validate_agent_name_invalid(self, invalid_name): def test_validate_agent_name_invalid(self, invalid_name):
"""Should raise ValueError for invalid agent names that don't match the pattern.""" """Should raise ValueError for invalid agent names that don't match the pattern."""
storage = FileMemoryStorage() storage = FileMemoryStorage()
@ -79,6 +80,7 @@ class TestFileMemoryStorage:
def test_load_creates_empty_memory(self, tmp_path): def test_load_creates_empty_memory(self, tmp_path):
"""Should create empty memory when file doesn't exist.""" """Should create empty memory when file doesn't exist."""
def mock_get_paths(): def mock_get_paths():
mock_paths = MagicMock() mock_paths = MagicMock()
mock_paths.memory_file = tmp_path / "non_existent_memory.json" mock_paths.memory_file = tmp_path / "non_existent_memory.json"
@ -125,10 +127,10 @@ class TestFileMemoryStorage:
# First load # First load
memory1 = storage.load() memory1 = storage.load()
assert memory1["facts"][0]["content"] == "initial fact" assert memory1["facts"][0]["content"] == "initial fact"
# Update file directly # Update file directly
memory_file.write_text('{"version": "1.0", "facts": [{"content": "updated fact"}]}') memory_file.write_text('{"version": "1.0", "facts": [{"content": "updated fact"}]}')
# Reload should get updated data # Reload should get updated data
memory2 = storage.reload() memory2 = storage.reload()
assert memory2["facts"][0]["content"] == "updated fact" assert memory2["facts"][0]["content"] == "updated fact"
@ -141,6 +143,7 @@ class TestGetMemoryStorage:
def reset_storage_instance(self): def reset_storage_instance(self):
"""Reset the global storage instance before and after each test.""" """Reset the global storage instance before and after each test."""
import deerflow.agents.memory.storage as storage_mod import deerflow.agents.memory.storage as storage_mod
storage_mod._storage_instance = None storage_mod._storage_instance = None
yield yield
storage_mod._storage_instance = None storage_mod._storage_instance = None
@ -167,6 +170,7 @@ class TestGetMemoryStorage:
def test_get_memory_storage_thread_safety(self): def test_get_memory_storage_thread_safety(self):
"""Should safely initialize the singleton even with concurrent calls.""" """Should safely initialize the singleton even with concurrent calls."""
results = [] results = []
def get_storage(): def get_storage():
# get_memory_storage is called concurrently from multiple threads while # get_memory_storage is called concurrently from multiple threads while
# get_memory_config is patched once around thread creation. This verifies # get_memory_config is patched once around thread creation. This verifies

1
frontend/.prettierignore Normal file
View File

@ -0,0 +1 @@
pnpm-lock.yaml

View File

@ -10,15 +10,15 @@ DeerFlow Frontend is a Next.js 16 web interface for an AI agent system. It commu
## Commands ## Commands
| Command | Purpose | | Command | Purpose |
|---------|---------| | ---------------- | ------------------------------------------------- |
| `pnpm dev` | Dev server with Turbopack (http://localhost:3000) | | `pnpm dev` | Dev server with Turbopack (http://localhost:3000) |
| `pnpm build` | Production build | | `pnpm build` | Production build |
| `pnpm check` | Lint + type check (run before committing) | | `pnpm check` | Lint + type check (run before committing) |
| `pnpm lint` | ESLint only | | `pnpm lint` | ESLint only |
| `pnpm lint:fix` | ESLint with auto-fix | | `pnpm lint:fix` | ESLint with auto-fix |
| `pnpm typecheck` | TypeScript type check (`tsc --noEmit`) | | `pnpm typecheck` | TypeScript type check (`tsc --noEmit`) |
| `pnpm start` | Start production server | | `pnpm start` | Start production server |
No test framework is configured. No test framework is configured.
@ -81,6 +81,7 @@ The frontend is a stateful chat application. Users create **threads** (conversat
## Environment ## Environment
Backend API URLs are optional; an nginx proxy is used by default: Backend API URLs are optional; an nginx proxy is used by default:
``` ```
NEXT_PUBLIC_BACKEND_BASE_URL=http://localhost:8001 NEXT_PUBLIC_BACKEND_BASE_URL=http://localhost:8001
NEXT_PUBLIC_LANGGRAPH_BASE_URL=http://localhost:2024 NEXT_PUBLIC_LANGGRAPH_BASE_URL=http://localhost:2024

View File

@ -114,17 +114,17 @@ src/
## Scripts ## Scripts
| Command | Description | | Command | Description |
|---------|-------------| | ------------------- | --------------------------------------- |
| `pnpm dev` | Start development server with Turbopack | | `pnpm dev` | Start development server with Turbopack |
| `pnpm build` | Build for production | | `pnpm build` | Build for production |
| `pnpm start` | Start production server | | `pnpm start` | Start production server |
| `pnpm format` | Check formatting with Prettier | | `pnpm format` | Check formatting with Prettier |
| `pnpm format:write` | Apply formatting with Prettier | | `pnpm format:write` | Apply formatting with Prettier |
| `pnpm lint` | Run ESLint | | `pnpm lint` | Run ESLint |
| `pnpm lint:fix` | Fix ESLint issues | | `pnpm lint:fix` | Fix ESLint issues |
| `pnpm typecheck` | Run TypeScript type checking | | `pnpm typecheck` | Run TypeScript type checking |
| `pnpm check` | Run both lint and typecheck | | `pnpm check` | Run both lint and typecheck |
## Development Notes ## Development Notes

View File

@ -1,6 +1,7 @@
# Dr. Fei-Fei Li: Recent Podcast Appearances Timeline (Last 6 Months) # Dr. Fei-Fei Li: Recent Podcast Appearances Timeline (Last 6 Months)
## Overview ## Overview
Dr. Fei-Fei Li, often called the "Godmother of AI," has been actively appearing on major podcasts discussing the future of artificial intelligence, spatial intelligence, human-centered AI, and her work at World Labs. This timeline compiles key highlights from her recent podcast appearances from August 2025 to January 2026. Dr. Fei-Fei Li, often called the "Godmother of AI," has been actively appearing on major podcasts discussing the future of artificial intelligence, spatial intelligence, human-centered AI, and her work at World Labs. This timeline compiles key highlights from her recent podcast appearances from August 2025 to January 2026.
--- ---
@ -8,9 +9,11 @@ Dr. Fei-Fei Li, often called the "Godmother of AI," has been actively appearing
## Timeline of Recent Podcast Appearances ## Timeline of Recent Podcast Appearances
### January 15, 2025 - **Possible Podcast** (with Reid Hoffman and Aria Finger) ### January 15, 2025 - **Possible Podcast** (with Reid Hoffman and Aria Finger)
**Episode:** "Fei-Fei Li on spatial intelligence and human-centered AI" **Episode:** "Fei-Fei Li on spatial intelligence and human-centered AI"
**Key Highlights:** **Key Highlights:**
- **Spatial Intelligence as Next Frontier:** Emphasized that spatial intelligence represents the next major evolution beyond large language models (LLMs) - **Spatial Intelligence as Next Frontier:** Emphasized that spatial intelligence represents the next major evolution beyond large language models (LLMs)
- **Human-Centered AI Philosophy:** Discussed the importance of building AI that amplifies human potential rather than replacing humans - **Human-Centered AI Philosophy:** Discussed the importance of building AI that amplifies human potential rather than replacing humans
- **Regulatory Guardrails:** Addressed the need for thoughtful regulation and governance frameworks for AI development - **Regulatory Guardrails:** Addressed the need for thoughtful regulation and governance frameworks for AI development
@ -22,9 +25,11 @@ Dr. Fei-Fei Li, often called the "Godmother of AI," has been actively appearing
--- ---
### August 15, 2025 - **Firing Line (PBS)** ### August 15, 2025 - **Firing Line (PBS)**
**Episode:** "Fei-Fei Li on ethical AI development" **Episode:** "Fei-Fei Li on ethical AI development"
**Key Highlights:** **Key Highlights:**
- **Ethical AI Development:** Discussed the challenges and responsibilities in developing AI ethically - **Ethical AI Development:** Discussed the challenges and responsibilities in developing AI ethically
- **Societal Impact:** Addressed how AI will transform various sectors including healthcare, education, and employment - **Societal Impact:** Addressed how AI will transform various sectors including healthcare, education, and employment
- **Policy Recommendations:** Provided insights on what policy frameworks are needed for responsible AI deployment - **Policy Recommendations:** Provided insights on what policy frameworks are needed for responsible AI deployment
@ -33,9 +38,11 @@ Dr. Fei-Fei Li, often called the "Godmother of AI," has been actively appearing
--- ---
### November 16, 2025 - **Lenny's Podcast** ### November 16, 2025 - **Lenny's Podcast**
**Episode:** "The Godmother of AI on jobs, robots & why world models are next" **Episode:** "The Godmother of AI on jobs, robots & why world models are next"
**Key Highlights:** **Key Highlights:**
- **World Models Introduction:** Explained why world models and spatial intelligence represent the next frontier beyond LLMs - **World Models Introduction:** Explained why world models and spatial intelligence represent the next frontier beyond LLMs
- **AI Won't Replace Humans:** Argued that AI won't replace humans but will require us to take responsibility for ourselves - **AI Won't Replace Humans:** Argued that AI won't replace humans but will require us to take responsibility for ourselves
- **Marble Applications:** Revealed surprising applications of World Labs' Marble product, from movie production to psychological research - **Marble Applications:** Revealed surprising applications of World Labs' Marble product, from movie production to psychological research
@ -44,6 +51,7 @@ Dr. Fei-Fei Li, often called the "Godmother of AI," has been actively appearing
- **Participation for All:** Explained how anyone can participate in AI regardless of their role or background - **Participation for All:** Explained how anyone can participate in AI regardless of their role or background
**Key Discussion Points:** **Key Discussion Points:**
1. How ImageNet helped spark the current AI explosion 1. How ImageNet helped spark the current AI explosion
2. The "bitter lesson" in AI and robotics 2. The "bitter lesson" in AI and robotics
3. Applications of Marble in creative industries and therapy 3. Applications of Marble in creative industries and therapy
@ -52,9 +60,11 @@ Dr. Fei-Fei Li, often called the "Godmother of AI," has been actively appearing
--- ---
### November 25, 2025 - **Masters of Scale Summit** ### November 25, 2025 - **Masters of Scale Summit**
**Episode:** "The 'Godmother of AI' on the next phase of AI" (with Reid Hoffman) **Episode:** "The 'Godmother of AI' on the next phase of AI" (with Reid Hoffman)
**Key Highlights:** **Key Highlights:**
- **Fearless Approach:** Discussed why scientists and entrepreneurs need to be fearless in the face of an uncertain AI future - **Fearless Approach:** Discussed why scientists and entrepreneurs need to be fearless in the face of an uncertain AI future
- **Spatial Intelligence & World Modeling:** Detailed the next phase of AI focusing on spatial understanding - **Spatial Intelligence & World Modeling:** Detailed the next phase of AI focusing on spatial understanding
- **Trust Building:** Explained how leaders should build societal trust in AI products and companies - **Trust Building:** Explained how leaders should build societal trust in AI products and companies
@ -62,6 +72,7 @@ Dr. Fei-Fei Li, often called the "Godmother of AI," has been actively appearing
- **Entrepreneurial Responsibility:** Argued that entrepreneurs should care about trust from day one of AI development - **Entrepreneurial Responsibility:** Argued that entrepreneurs should care about trust from day one of AI development
**Chapter Topics Covered:** **Chapter Topics Covered:**
- The next phase of AI: spatial intelligence & world modeling - The next phase of AI: spatial intelligence & world modeling
- What spatial intelligence has done for humans - What spatial intelligence has done for humans
- Whether AI is over-hyped - Whether AI is over-hyped
@ -71,9 +82,11 @@ Dr. Fei-Fei Li, often called the "Godmother of AI," has been actively appearing
--- ---
### December 9, 2025 - **The Tim Ferriss Show** (#839) ### December 9, 2025 - **The Tim Ferriss Show** (#839)
**Episode:** "Dr. Fei-Fei Li, The Godmother of AI — Asking Audacious Questions, Civilizational Technology, and Finding Your North Star" **Episode:** "Dr. Fei-Fei Li, The Godmother of AI — Asking Audacious Questions, Civilizational Technology, and Finding Your North Star"
**Key Highlights:** **Key Highlights:**
- **Civilizational Technology:** Defined AI as a "civilizational technology" that will have profound economic, social, cultural, and political impacts - **Civilizational Technology:** Defined AI as a "civilizational technology" that will have profound economic, social, cultural, and political impacts
- **Personal Journey:** Shared her immigrant story from Chengdu to New Jersey, and her family's seven years running a dry cleaning shop while she attended Princeton - **Personal Journey:** Shared her immigrant story from Chengdu to New Jersey, and her family's seven years running a dry cleaning shop while she attended Princeton
- **ImageNet Creation:** Detailed the creation of ImageNet and how it birthed modern AI, including innovative use of Amazon Mechanical Turk for data labeling - **ImageNet Creation:** Detailed the creation of ImageNet and how it birthed modern AI, including innovative use of Amazon Mechanical Turk for data labeling
@ -82,11 +95,13 @@ Dr. Fei-Fei Li, often called the "Godmother of AI," has been actively appearing
- **Human-Centered Focus:** Emphasized that "people are at the heart of everything" in AI development - **Human-Centered Focus:** Emphasized that "people are at the heart of everything" in AI development
**Notable Quotes:** **Notable Quotes:**
- "Really, at the end of the day, people are at the heart of everything. People made AI, people will be using AI, people will be impacted by AI, and people should have a say in AI." - "Really, at the end of the day, people are at the heart of everything. People made AI, people will be using AI, people will be impacted by AI, and people should have a say in AI."
- "AI is absolutely a civilizational technology... it'll have—or [is] already having—a profound impact in the economic, social, cultural, political, downstream effects of our society." - "AI is absolutely a civilizational technology... it'll have—or [is] already having—a profound impact in the economic, social, cultural, political, downstream effects of our society."
- "What is your North Star?" - "What is your North Star?"
**Key Topics Discussed:** **Key Topics Discussed:**
- From fighter jets to physics to asking "What is intelligence?" - From fighter jets to physics to asking "What is intelligence?"
- The epiphany everyone missed: Big data as the hidden hypothesis - The epiphany everyone missed: Big data as the hidden hypothesis
- Against the single-genius myth: Science as non-linear lineage - Against the single-genius myth: Science as non-linear lineage
@ -97,9 +112,11 @@ Dr. Fei-Fei Li, often called the "Godmother of AI," has been actively appearing
--- ---
### June 16, 2025 - **Y Combinator Startup Podcast** ### June 16, 2025 - **Y Combinator Startup Podcast**
**Episode:** "Fei-Fei Li - Spatial Intelligence is the Next Frontier in AI" **Episode:** "Fei-Fei Li - Spatial Intelligence is the Next Frontier in AI"
**Key Highlights:** **Key Highlights:**
- **Startup Perspective:** Provided insights for AI startups on navigating the current landscape - **Startup Perspective:** Provided insights for AI startups on navigating the current landscape
- **Technical Deep Dive:** Offered detailed explanations of spatial intelligence technologies - **Technical Deep Dive:** Offered detailed explanations of spatial intelligence technologies
- **Entrepreneurial Advice:** Shared lessons from transitioning from academia to entrepreneurship - **Entrepreneurial Advice:** Shared lessons from transitioning from academia to entrepreneurship
@ -110,26 +127,31 @@ Dr. Fei-Fei Li, often called the "Godmother of AI," has been actively appearing
## Common Themes Across Recent Appearances ## Common Themes Across Recent Appearances
### 1. **Spatial Intelligence as the Next Frontier** ### 1. **Spatial Intelligence as the Next Frontier**
- Repeated emphasis that spatial intelligence represents the next major evolution beyond language models - Repeated emphasis that spatial intelligence represents the next major evolution beyond language models
- World Labs' focus on creating AI that understands and interacts with the physical world - World Labs' focus on creating AI that understands and interacts with the physical world
- Applications ranging from robotics and autonomous systems to creative industries and therapy - Applications ranging from robotics and autonomous systems to creative industries and therapy
### 2. **Human-Centered AI Philosophy** ### 2. **Human-Centered AI Philosophy**
- Consistent message that AI should augment rather than replace human capabilities - Consistent message that AI should augment rather than replace human capabilities
- Emphasis on maintaining human agency and responsibility in AI systems - Emphasis on maintaining human agency and responsibility in AI systems
- Focus on building trust and ethical frameworks - Focus on building trust and ethical frameworks
### 3. **Educational Transformation** ### 3. **Educational Transformation**
- Advocacy for integrating AI into education to enhance learning - Advocacy for integrating AI into education to enhance learning
- Proposal to use AI as a benchmark for student improvement - Proposal to use AI as a benchmark for student improvement
- Emphasis on making AI accessible to people from all backgrounds - Emphasis on making AI accessible to people from all backgrounds
### 4. **Historical Perspective** ### 4. **Historical Perspective**
- Frequent references to ImageNet's role in sparking the deep learning revolution - Frequent references to ImageNet's role in sparking the deep learning revolution
- Context about how rapidly the AI landscape has changed - Context about how rapidly the AI landscape has changed
- Emphasis on collaborative, non-linear progress in scientific advancement - Emphasis on collaborative, non-linear progress in scientific advancement
### 5. **Entrepreneurial Vision** ### 5. **Entrepreneurial Vision**
- Insights on building AI companies in the current environment - Insights on building AI companies in the current environment
- Balance between technological innovation and responsible development - Balance between technological innovation and responsible development
- Focus on practical applications that solve real-world problems - Focus on practical applications that solve real-world problems
@ -139,18 +161,21 @@ Dr. Fei-Fei Li, often called the "Godmother of AI," has been actively appearing
## Key Insights and Predictions ## Key Insights and Predictions
### **Near-Term Developments (1-3 years):** ### **Near-Term Developments (1-3 years):**
- Rapid advancement in spatial intelligence and world modeling technologies - Rapid advancement in spatial intelligence and world modeling technologies
- Increased integration of AI in education and creative industries - Increased integration of AI in education and creative industries
- Growing focus on AI ethics and governance frameworks - Growing focus on AI ethics and governance frameworks
- Expansion of practical applications in healthcare, therapy, and accessibility - Expansion of practical applications in healthcare, therapy, and accessibility
### **Medium-Term Vision (3-5 years):** ### **Medium-Term Vision (3-5 years):**
- More sophisticated human-AI collaboration systems - More sophisticated human-AI collaboration systems
- Breakthroughs in robotics enabled by spatial intelligence - Breakthroughs in robotics enabled by spatial intelligence
- Transformation of how we teach and learn with AI assistance - Transformation of how we teach and learn with AI assistance
- Development of new industries centered around spatial AI - Development of new industries centered around spatial AI
### **Long-Term Philosophy:** ### **Long-Term Philosophy:**
- AI as a "civilizational technology" that requires thoughtful stewardship - AI as a "civilizational technology" that requires thoughtful stewardship
- Emphasis on maintaining human values and agency in technological progress - Emphasis on maintaining human values and agency in technological progress
- Vision of technology that helps humanity "raise above our paleolithic emotions" - Vision of technology that helps humanity "raise above our paleolithic emotions"
@ -166,6 +191,7 @@ The timeline shows her evolving role from academic researcher to entrepreneur wh
--- ---
## Sources ## Sources
1. The Tim Ferriss Show (December 9, 2025) 1. The Tim Ferriss Show (December 9, 2025)
2. Lenny's Podcast (November 16, 2025) 2. Lenny's Podcast (November 16, 2025)
3. Masters of Scale Summit (November 25, 2025) 3. Masters of Scale Summit (November 25, 2025)
@ -173,4 +199,4 @@ The timeline shows her evolving role from academic researcher to entrepreneur wh
5. Firing Line, PBS (August 15, 2025) 5. Firing Line, PBS (August 15, 2025)
6. Y Combinator Startup Podcast (June 16, 2025) 6. Y Combinator Startup Podcast (June 16, 2025)
*Compiled on January 25, 2026* _Compiled on January 25, 2026_

View File

@ -802,4 +802,4 @@
"interrupts": [], "interrupts": [],
"checkpoint_id": "1f0f46d7-77ea-64ca-802c-0462f9bf4fdd", "checkpoint_id": "1f0f46d7-77ea-64ca-802c-0462f9bf4fdd",
"parent_checkpoint_id": "1f0f46d7-77e2-6496-802b-68a165ed83e9" "parent_checkpoint_id": "1f0f46d7-77e2-6496-802b-68a165ed83e9"
} }

View File

@ -1 +1,4 @@
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>⚽</text></svg>"> <link
rel="icon"
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22></text></svg>"
/>

View File

@ -1,365 +1,385 @@
<!DOCTYPE html> <!doctype html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>江苏城市足球联赛2025赛季 | 苏超联赛第一季</title> <title>江苏城市足球联赛2025赛季 | 苏超联赛第一季</title>
<link rel="stylesheet" href="css/style.css"> <link rel="stylesheet" href="css/style.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <link
<link rel="preconnect" href="https://fonts.googleapis.com"> rel="stylesheet"
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700;800;900&family=Oswald:wght@300;400;500;600;700&family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css"> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>⚽</text></svg>"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
</head> <link
<body> href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700;800;900&family=Oswald:wght@300;400;500;600;700&family=Inter:wght@300;400;500;600;700&display=swap"
rel="stylesheet"
/>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css"
/>
<link
rel="icon"
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22></text></svg>"
/>
</head>
<body>
<!-- 加载动画 --> <!-- 加载动画 -->
<div class="loader"> <div class="loader">
<div class="loader-content"> <div class="loader-content">
<div class="football"></div> <div class="football"></div>
<div class="loader-text">加载中...</div> <div class="loader-text">加载中...</div>
</div> </div>
</div> </div>
<!-- 导航栏 --> <!-- 导航栏 -->
<nav class="navbar"> <nav class="navbar">
<div class="container"> <div class="container">
<div class="nav-brand"> <div class="nav-brand">
<div class="logo"> <div class="logo">
<div class="logo-ball"></div> <div class="logo-ball"></div>
<span class="logo-text">苏超联赛</span> <span class="logo-text">苏超联赛</span>
</div> </div>
<div class="league-name">江苏城市足球联赛2025赛季</div> <div class="league-name">江苏城市足球联赛2025赛季</div>
</div>
<div class="nav-menu">
<a href="#home" class="nav-link active">首页</a>
<a href="#teams" class="nav-link">球队</a>
<a href="#fixtures" class="nav-link">赛程</a>
<a href="#standings" class="nav-link">积分榜</a>
<a href="#stats" class="nav-link">数据</a>
<a href="#news" class="nav-link">新闻</a>
</div>
<div class="nav-actions">
<button class="btn-theme-toggle">
<i class="fas fa-moon"></i>
</button>
<button class="btn-menu-toggle">
<i class="fas fa-bars"></i>
</button>
</div>
</div> </div>
<div class="nav-menu">
<a href="#home" class="nav-link active">首页</a>
<a href="#teams" class="nav-link">球队</a>
<a href="#fixtures" class="nav-link">赛程</a>
<a href="#standings" class="nav-link">积分榜</a>
<a href="#stats" class="nav-link">数据</a>
<a href="#news" class="nav-link">新闻</a>
</div>
<div class="nav-actions">
<button class="btn-theme-toggle">
<i class="fas fa-moon"></i>
</button>
<button class="btn-menu-toggle">
<i class="fas fa-bars"></i>
</button>
</div>
</div>
</nav> </nav>
<!-- 主内容区 --> <!-- 主内容区 -->
<main> <main>
<!-- 英雄区域 --> <!-- 英雄区域 -->
<section id="home" class="hero"> <section id="home" class="hero">
<div class="hero-background"> <div class="hero-background">
<div class="hero-gradient"></div> <div class="hero-gradient"></div>
<div class="hero-pattern"></div> <div class="hero-pattern"></div>
<div class="hero-ball-animation"></div> <div class="hero-ball-animation"></div>
</div> </div>
<div class="container">
<div class="hero-content">
<div class="hero-badge">
<span class="badge-season">2025赛季</span>
<span class="badge-league">苏超联赛第一季</span>
</div>
<h1 class="hero-title">
<span class="title-line">江苏城市</span>
<span class="title-line highlight">足球联赛</span>
</h1>
<p class="hero-subtitle">
江苏省首个城市间职业足球联赛汇集12支精英球队点燃2025赛季战火
</p>
<div class="hero-stats">
<div class="stat-item">
<div class="stat-number">12</div>
<div class="stat-label">参赛球队</div>
</div>
<div class="stat-item">
<div class="stat-number">132</div>
<div class="stat-label">场比赛</div>
</div>
<div class="stat-item">
<div class="stat-number">26</div>
<div class="stat-label">比赛周</div>
</div>
<div class="stat-item">
<div class="stat-number">1</div>
<div class="stat-label">冠军荣耀</div>
</div>
</div>
<div class="hero-actions">
<a href="#fixtures" class="btn btn-primary">
<i class="fas fa-calendar-alt"></i>
查看赛程
</a>
<a href="#standings" class="btn btn-secondary">
<i class="fas fa-trophy"></i>
积分榜
</a>
</div>
</div>
<div class="hero-visual">
<div class="stadium-visual">
<div class="stadium-field"></div>
<div class="stadium-stands"></div>
<div class="stadium-players">
<div class="player player-1"></div>
<div class="player player-2"></div>
<div class="player player-3"></div>
</div>
<div class="stadium-ball"></div>
</div>
</div>
</div>
<div class="hero-scroll">
<div class="scroll-indicator">
<div class="scroll-line"></div>
</div>
</div>
</section>
<!-- 下一场比赛 --> <div class="container">
<section class="next-match"> <div class="hero-content">
<div class="container"> <div class="hero-badge">
<div class="section-header"> <span class="badge-season">2025赛季</span>
<h2 class="section-title">下一场比赛</h2> <span class="badge-league">苏超联赛第一季</span>
<div class="section-subtitle">即将开始的精彩对决</div>
</div>
<div class="match-card">
<div class="match-date">
<div class="match-day">周六</div>
<div class="match-date-number">25</div>
<div class="match-month">一月</div>
<div class="match-time">19:30</div>
</div>
<div class="match-teams">
<div class="team team-home">
<div class="team-logo logo-nanjing"></div>
<div class="team-name">南京城联</div>
<div class="team-record">8胜 3平 2负</div>
</div>
<div class="match-vs">
<div class="vs-text">VS</div>
<div class="match-info">
<div class="match-venue">南京奥体中心</div>
<div class="match-round">第12轮</div>
</div>
</div>
<div class="team team-away">
<div class="team-logo logo-suzhou"></div>
<div class="team-name">苏州雄狮</div>
<div class="team-record">7胜 4平 2负</div>
</div>
</div>
<div class="match-actions">
<button class="btn btn-outline">
<i class="fas fa-bell"></i>
设置提醒
</button>
<button class="btn btn-primary">
<i class="fas fa-ticket-alt"></i>
购票
</button>
</div>
</div>
</div> </div>
</section>
<!-- 球队展示 --> <h1 class="hero-title">
<section id="teams" class="teams-section"> <span class="title-line">江苏城市</span>
<div class="container"> <span class="title-line highlight">足球联赛</span>
<div class="section-header"> </h1>
<h2 class="section-title">参赛球队</h2>
<div class="section-subtitle">12支城市代表队的荣耀之战</div>
</div>
<div class="teams-grid">
<!-- 球队卡片将通过JS动态生成 -->
</div>
</div>
</section>
<!-- 积分榜 --> <p class="hero-subtitle">
<section id="standings" class="standings-section"> 江苏省首个城市间职业足球联赛汇集12支精英球队点燃2025赛季战火
<div class="container"> </p>
<div class="section-header">
<h2 class="section-title">积分榜</h2>
<div class="section-subtitle">2025赛季实时排名</div>
</div>
<div class="standings-container">
<div class="standings-table">
<table>
<thead>
<tr>
<th>排名</th>
<th>球队</th>
<th>场次</th>
<th></th>
<th></th>
<th></th>
<th>进球</th>
<th>失球</th>
<th>净胜球</th>
<th>积分</th>
</tr>
</thead>
<tbody>
<!-- 积分榜数据将通过JS动态生成 -->
</tbody>
</table>
</div>
</div>
</div>
</section>
<!-- 赛程表 --> <div class="hero-stats">
<section id="fixtures" class="fixtures-section"> <div class="stat-item">
<div class="container"> <div class="stat-number">12</div>
<div class="section-header"> <div class="stat-label">参赛球队</div>
<h2 class="section-title">赛程表</h2> </div>
<div class="section-subtitle">2025赛季完整赛程</div> <div class="stat-item">
</div> <div class="stat-number">132</div>
<div class="stat-label">场比赛</div>
<div class="fixtures-tabs"> </div>
<div class="tabs"> <div class="stat-item">
<button class="tab active" data-round="all">全部赛程</button> <div class="stat-number">26</div>
<button class="tab" data-round="next">即将比赛</button> <div class="stat-label">比赛周</div>
<button class="tab" data-round="recent">最近赛果</button> </div>
</div> <div class="stat-item">
<div class="stat-number">1</div>
<div class="fixtures-list"> <div class="stat-label">冠军荣耀</div>
<!-- 赛程数据将通过JS动态生成 --> </div>
</div>
</div>
</div> </div>
</section>
<!-- 数据统计 --> <div class="hero-actions">
<section id="stats" class="stats-section"> <a href="#fixtures" class="btn btn-primary">
<div class="container"> <i class="fas fa-calendar-alt"></i>
<div class="section-header"> 查看赛程
<h2 class="section-title">数据统计</h2> </a>
<div class="section-subtitle">球员与球队数据排行榜</div> <a href="#standings" class="btn btn-secondary">
</div> <i class="fas fa-trophy"></i>
积分榜
<div class="stats-tabs"> </a>
<div class="stats-tab-nav">
<button class="stats-tab active" data-tab="scorers">射手榜</button>
<button class="stats-tab" data-tab="assists">助攻榜</button>
<button class="stats-tab" data-tab="teams">球队数据</button>
</div>
<div class="stats-content">
<div class="stats-tab-content active" id="scorers">
<!-- 射手榜数据 -->
</div>
<div class="stats-tab-content" id="assists">
<!-- 助攻榜数据 -->
</div>
<div class="stats-tab-content" id="teams">
<!-- 球队数据 -->
</div>
</div>
</div>
</div> </div>
</section> </div>
<!-- 新闻动态 --> <div class="hero-visual">
<section id="news" class="news-section"> <div class="stadium-visual">
<div class="container"> <div class="stadium-field"></div>
<div class="section-header"> <div class="stadium-stands"></div>
<h2 class="section-title">新闻动态</h2> <div class="stadium-players">
<div class="section-subtitle">联赛最新资讯</div> <div class="player player-1"></div>
</div> <div class="player player-2"></div>
<div class="player player-3"></div>
<div class="news-grid"> </div>
<!-- 新闻卡片将通过JS动态生成 --> <div class="stadium-ball"></div>
</div>
</div> </div>
</section> </div>
</div>
<!-- 底部 --> <div class="hero-scroll">
<footer class="footer"> <div class="scroll-indicator">
<div class="container"> <div class="scroll-line"></div>
<div class="footer-content"> </div>
<div class="footer-brand"> </div>
<div class="logo"> </section>
<div class="logo-ball"></div>
<span class="logo-text">苏超联赛</span> <!-- 下一场比赛 -->
</div> <section class="next-match">
<div class="footer-description"> <div class="container">
江苏城市足球联赛2025赛季官方网站 <div class="section-header">
</div> <h2 class="section-title">下一场比赛</h2>
<div class="footer-social"> <div class="section-subtitle">即将开始的精彩对决</div>
<a href="#" class="social-link"><i class="fab fa-weibo"></i></a> </div>
<a href="#" class="social-link"><i class="fab fa-weixin"></i></a>
<a href="#" class="social-link"><i class="fab fa-douyin"></i></a> <div class="match-card">
<a href="#" class="social-link"><i class="fab fa-bilibili"></i></a> <div class="match-date">
</div> <div class="match-day">周六</div>
</div> <div class="match-date-number">25</div>
<div class="match-month">一月</div>
<div class="footer-links"> <div class="match-time">19:30</div>
<div class="footer-column">
<h3 class="footer-title">联赛信息</h3>
<a href="#" class="footer-link">关于联赛</a>
<a href="#" class="footer-link">联赛章程</a>
<a href="#" class="footer-link">组织机构</a>
<a href="#" class="footer-link">合作伙伴</a>
</div>
<div class="footer-column">
<h3 class="footer-title">球迷服务</h3>
<a href="#" class="footer-link">票务信息</a>
<a href="#" class="footer-link">球迷社区</a>
<a href="#" class="footer-link">官方商店</a>
<a href="#" class="footer-link">联系我们</a>
</div>
<div class="footer-column">
<h3 class="footer-title">媒体中心</h3>
<a href="#" class="footer-link">新闻发布</a>
<a href="#" class="footer-link">媒体资料</a>
<a href="#" class="footer-link">采访申请</a>
<a href="#" class="footer-link">摄影图库</a>
</div>
</div>
</div>
<div class="footer-bottom">
<div class="copyright">
&copy; 2025 江苏城市足球联赛. 保留所有权利.
</div>
<div class="footer-legal">
<a href="#" class="legal-link">隐私政策</a>
<a href="#" class="legal-link">使用条款</a>
<a href="#" class="legal-link">Cookie政策</a>
</div>
</div>
</div> </div>
</footer>
<div class="match-teams">
<div class="team team-home">
<div class="team-logo logo-nanjing"></div>
<div class="team-name">南京城联</div>
<div class="team-record">8胜 3平 2负</div>
</div>
<div class="match-vs">
<div class="vs-text">VS</div>
<div class="match-info">
<div class="match-venue">南京奥体中心</div>
<div class="match-round">第12轮</div>
</div>
</div>
<div class="team team-away">
<div class="team-logo logo-suzhou"></div>
<div class="team-name">苏州雄狮</div>
<div class="team-record">7胜 4平 2负</div>
</div>
</div>
<div class="match-actions">
<button class="btn btn-outline">
<i class="fas fa-bell"></i>
设置提醒
</button>
<button class="btn btn-primary">
<i class="fas fa-ticket-alt"></i>
购票
</button>
</div>
</div>
</div>
</section>
<!-- 球队展示 -->
<section id="teams" class="teams-section">
<div class="container">
<div class="section-header">
<h2 class="section-title">参赛球队</h2>
<div class="section-subtitle">12支城市代表队的荣耀之战</div>
</div>
<div class="teams-grid">
<!-- 球队卡片将通过JS动态生成 -->
</div>
</div>
</section>
<!-- 积分榜 -->
<section id="standings" class="standings-section">
<div class="container">
<div class="section-header">
<h2 class="section-title">积分榜</h2>
<div class="section-subtitle">2025赛季实时排名</div>
</div>
<div class="standings-container">
<div class="standings-table">
<table>
<thead>
<tr>
<th>排名</th>
<th>球队</th>
<th>场次</th>
<th></th>
<th></th>
<th></th>
<th>进球</th>
<th>失球</th>
<th>净胜球</th>
<th>积分</th>
</tr>
</thead>
<tbody>
<!-- 积分榜数据将通过JS动态生成 -->
</tbody>
</table>
</div>
</div>
</div>
</section>
<!-- 赛程表 -->
<section id="fixtures" class="fixtures-section">
<div class="container">
<div class="section-header">
<h2 class="section-title">赛程表</h2>
<div class="section-subtitle">2025赛季完整赛程</div>
</div>
<div class="fixtures-tabs">
<div class="tabs">
<button class="tab active" data-round="all">全部赛程</button>
<button class="tab" data-round="next">即将比赛</button>
<button class="tab" data-round="recent">最近赛果</button>
</div>
<div class="fixtures-list">
<!-- 赛程数据将通过JS动态生成 -->
</div>
</div>
</div>
</section>
<!-- 数据统计 -->
<section id="stats" class="stats-section">
<div class="container">
<div class="section-header">
<h2 class="section-title">数据统计</h2>
<div class="section-subtitle">球员与球队数据排行榜</div>
</div>
<div class="stats-tabs">
<div class="stats-tab-nav">
<button class="stats-tab active" data-tab="scorers">
射手榜
</button>
<button class="stats-tab" data-tab="assists">助攻榜</button>
<button class="stats-tab" data-tab="teams">球队数据</button>
</div>
<div class="stats-content">
<div class="stats-tab-content active" id="scorers">
<!-- 射手榜数据 -->
</div>
<div class="stats-tab-content" id="assists">
<!-- 助攻榜数据 -->
</div>
<div class="stats-tab-content" id="teams">
<!-- 球队数据 -->
</div>
</div>
</div>
</div>
</section>
<!-- 新闻动态 -->
<section id="news" class="news-section">
<div class="container">
<div class="section-header">
<h2 class="section-title">新闻动态</h2>
<div class="section-subtitle">联赛最新资讯</div>
</div>
<div class="news-grid">
<!-- 新闻卡片将通过JS动态生成 -->
</div>
</div>
</section>
<!-- 底部 -->
<footer class="footer">
<div class="container">
<div class="footer-content">
<div class="footer-brand">
<div class="logo">
<div class="logo-ball"></div>
<span class="logo-text">苏超联赛</span>
</div>
<div class="footer-description">
江苏城市足球联赛2025赛季官方网站
</div>
<div class="footer-social">
<a href="#" class="social-link"><i class="fab fa-weibo"></i></a>
<a href="#" class="social-link"
><i class="fab fa-weixin"></i
></a>
<a href="#" class="social-link"
><i class="fab fa-douyin"></i
></a>
<a href="#" class="social-link"
><i class="fab fa-bilibili"></i
></a>
</div>
</div>
<div class="footer-links">
<div class="footer-column">
<h3 class="footer-title">联赛信息</h3>
<a href="#" class="footer-link">关于联赛</a>
<a href="#" class="footer-link">联赛章程</a>
<a href="#" class="footer-link">组织机构</a>
<a href="#" class="footer-link">合作伙伴</a>
</div>
<div class="footer-column">
<h3 class="footer-title">球迷服务</h3>
<a href="#" class="footer-link">票务信息</a>
<a href="#" class="footer-link">球迷社区</a>
<a href="#" class="footer-link">官方商店</a>
<a href="#" class="footer-link">联系我们</a>
</div>
<div class="footer-column">
<h3 class="footer-title">媒体中心</h3>
<a href="#" class="footer-link">新闻发布</a>
<a href="#" class="footer-link">媒体资料</a>
<a href="#" class="footer-link">采访申请</a>
<a href="#" class="footer-link">摄影图库</a>
</div>
</div>
</div>
<div class="footer-bottom">
<div class="copyright">
&copy; 2025 江苏城市足球联赛. 保留所有权利.
</div>
<div class="footer-legal">
<a href="#" class="legal-link">隐私政策</a>
<a href="#" class="legal-link">使用条款</a>
<a href="#" class="legal-link">Cookie政策</a>
</div>
</div>
</div>
</footer>
</main> </main>
<!-- JavaScript文件 --> <!-- JavaScript文件 -->
<script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js"></script>
<script src="js/data.js"></script> <script src="js/data.js"></script>
<script src="js/main.js"></script> <script src="js/main.js"></script>
</body> </body>
</html> </html>

View File

@ -1,163 +1,163 @@
// 江苏城市足球联赛2025赛季 - 主JavaScript文件 // 江苏城市足球联赛2025赛季 - 主JavaScript文件
document.addEventListener('DOMContentLoaded', function() { document.addEventListener("DOMContentLoaded", function () {
// 初始化加载动画 // 初始化加载动画
initLoader(); initLoader();
// 初始化主题切换 // 初始化主题切换
initThemeToggle(); initThemeToggle();
// 初始化导航菜单 // 初始化导航菜单
initNavigation(); initNavigation();
// 初始化滚动监听 // 初始化滚动监听
initScrollSpy(); initScrollSpy();
// 渲染球队卡片 // 渲染球队卡片
renderTeams(); renderTeams();
// 渲染积分榜 // 渲染积分榜
renderStandings(); renderStandings();
// 渲染赛程表 // 渲染赛程表
renderFixtures(); renderFixtures();
// 渲染数据统计 // 渲染数据统计
renderStats(); renderStats();
// 渲染新闻动态 // 渲染新闻动态
renderNews(); renderNews();
// 初始化标签页切换 // 初始化标签页切换
initTabs(); initTabs();
// 初始化移动端菜单 // 初始化移动端菜单
initMobileMenu(); initMobileMenu();
}); });
// 加载动画 // 加载动画
function initLoader() { function initLoader() {
const loader = document.querySelector('.loader'); const loader = document.querySelector(".loader");
// 模拟加载延迟 // 模拟加载延迟
setTimeout(() => {
loader.classList.add("loaded");
// 动画结束后隐藏loader
setTimeout(() => { setTimeout(() => {
loader.classList.add('loaded'); loader.style.display = "none";
}, 300);
// 动画结束后隐藏loader }, 1500);
setTimeout(() => {
loader.style.display = 'none';
}, 300);
}, 1500);
} }
// 主题切换 // 主题切换
function initThemeToggle() { function initThemeToggle() {
const themeToggle = document.querySelector('.btn-theme-toggle'); const themeToggle = document.querySelector(".btn-theme-toggle");
const themeIcon = themeToggle.querySelector('i'); const themeIcon = themeToggle.querySelector("i");
// 检查本地存储的主题偏好 // 检查本地存储的主题偏好
const savedTheme = localStorage.getItem('theme') || 'light'; const savedTheme = localStorage.getItem("theme") || "light";
document.documentElement.setAttribute('data-theme', savedTheme); document.documentElement.setAttribute("data-theme", savedTheme);
updateThemeIcon(savedTheme); updateThemeIcon(savedTheme);
themeToggle.addEventListener('click', () => { themeToggle.addEventListener("click", () => {
const currentTheme = document.documentElement.getAttribute('data-theme'); const currentTheme = document.documentElement.getAttribute("data-theme");
const newTheme = currentTheme === 'light' ? 'dark' : 'light'; const newTheme = currentTheme === "light" ? "dark" : "light";
document.documentElement.setAttribute('data-theme', newTheme); document.documentElement.setAttribute("data-theme", newTheme);
localStorage.setItem('theme', newTheme); localStorage.setItem("theme", newTheme);
updateThemeIcon(newTheme); updateThemeIcon(newTheme);
// 添加切换动画 // 添加切换动画
themeToggle.style.transform = 'scale(0.9)'; themeToggle.style.transform = "scale(0.9)";
setTimeout(() => { setTimeout(() => {
themeToggle.style.transform = ''; themeToggle.style.transform = "";
}, 150); }, 150);
}); });
function updateThemeIcon(theme) { function updateThemeIcon(theme) {
if (theme === 'dark') { if (theme === "dark") {
themeIcon.className = 'fas fa-sun'; themeIcon.className = "fas fa-sun";
} else { } else {
themeIcon.className = 'fas fa-moon'; themeIcon.className = "fas fa-moon";
}
} }
}
} }
// 导航菜单 // 导航菜单
function initNavigation() { function initNavigation() {
const navLinks = document.querySelectorAll('.nav-link'); const navLinks = document.querySelectorAll(".nav-link");
navLinks.forEach(link => { navLinks.forEach((link) => {
link.addEventListener('click', function(e) { link.addEventListener("click", function (e) {
e.preventDefault(); e.preventDefault();
const targetId = this.getAttribute('href'); const targetId = this.getAttribute("href");
const targetSection = document.querySelector(targetId); const targetSection = document.querySelector(targetId);
if (targetSection) { if (targetSection) {
// 更新活动链接 // 更新活动链接
navLinks.forEach(l => l.classList.remove('active')); navLinks.forEach((l) => l.classList.remove("active"));
this.classList.add('active'); this.classList.add("active");
// 平滑滚动到目标区域 // 平滑滚动到目标区域
window.scrollTo({ window.scrollTo({
top: targetSection.offsetTop - 80, top: targetSection.offsetTop - 80,
behavior: 'smooth' behavior: "smooth",
});
// 如果是移动端,关闭菜单
const navMenu = document.querySelector('.nav-menu');
if (navMenu.classList.contains('active')) {
navMenu.classList.remove('active');
}
}
}); });
// 如果是移动端,关闭菜单
const navMenu = document.querySelector(".nav-menu");
if (navMenu.classList.contains("active")) {
navMenu.classList.remove("active");
}
}
}); });
});
} }
// 滚动监听 // 滚动监听
function initScrollSpy() { function initScrollSpy() {
const sections = document.querySelectorAll('section[id]'); const sections = document.querySelectorAll("section[id]");
const navLinks = document.querySelectorAll('.nav-link'); const navLinks = document.querySelectorAll(".nav-link");
window.addEventListener('scroll', () => { window.addEventListener("scroll", () => {
let current = ''; let current = "";
sections.forEach(section => { sections.forEach((section) => {
const sectionTop = section.offsetTop; const sectionTop = section.offsetTop;
const sectionHeight = section.clientHeight; const sectionHeight = section.clientHeight;
if (scrollY >= sectionTop - 100) { if (scrollY >= sectionTop - 100) {
current = section.getAttribute('id'); current = section.getAttribute("id");
} }
});
navLinks.forEach(link => {
link.classList.remove('active');
if (link.getAttribute('href') === `#${current}`) {
link.classList.add('active');
}
});
}); });
navLinks.forEach((link) => {
link.classList.remove("active");
if (link.getAttribute("href") === `#${current}`) {
link.classList.add("active");
}
});
});
} }
// 渲染球队卡片 // 渲染球队卡片
function renderTeams() { function renderTeams() {
const teamsGrid = document.querySelector('.teams-grid'); const teamsGrid = document.querySelector(".teams-grid");
if (!teamsGrid) return; if (!teamsGrid) return;
teamsGrid.innerHTML = ''; teamsGrid.innerHTML = "";
leagueData.teams.forEach(team => { leagueData.teams.forEach((team) => {
const teamCard = document.createElement('div'); const teamCard = document.createElement("div");
teamCard.className = 'team-card'; teamCard.className = "team-card";
// 获取球队统计数据 // 获取球队统计数据
const standing = leagueData.standings.find(s => s.teamId === team.id); const standing = leagueData.standings.find((s) => s.teamId === team.id);
teamCard.innerHTML = ` teamCard.innerHTML = `
<div class="team-card-logo" style="background: linear-gradient(135deg, ${team.colors[0]} 0%, ${team.colors[1]} 100%);"> <div class="team-card-logo" style="background: linear-gradient(135deg, ${team.colors[0]} 0%, ${team.colors[1]} 100%);">
${team.shortName} ${team.shortName}
</div> </div>
@ -165,52 +165,52 @@ function renderTeams() {
<div class="team-card-city">${team.city}</div> <div class="team-card-city">${team.city}</div>
<div class="team-card-stats"> <div class="team-card-stats">
<div class="team-stat"> <div class="team-stat">
<div class="team-stat-value">${standing ? standing.rank : '-'}</div> <div class="team-stat-value">${standing ? standing.rank : "-"}</div>
<div class="team-stat-label">排名</div> <div class="team-stat-label">排名</div>
</div> </div>
<div class="team-stat"> <div class="team-stat">
<div class="team-stat-value">${standing ? standing.points : '0'}</div> <div class="team-stat-value">${standing ? standing.points : "0"}</div>
<div class="team-stat-label">积分</div> <div class="team-stat-label">积分</div>
</div> </div>
<div class="team-stat"> <div class="team-stat">
<div class="team-stat-value">${standing ? standing.goalDifference : '0'}</div> <div class="team-stat-value">${standing ? standing.goalDifference : "0"}</div>
<div class="team-stat-label">净胜球</div> <div class="team-stat-label">净胜球</div>
</div> </div>
</div> </div>
`; `;
teamCard.addEventListener('click', () => { teamCard.addEventListener("click", () => {
// 这里可以添加点击跳转到球队详情页的功能 // 这里可以添加点击跳转到球队详情页的功能
alert(`查看 ${team.name} 的详细信息`); alert(`查看 ${team.name} 的详细信息`);
});
teamsGrid.appendChild(teamCard);
}); });
teamsGrid.appendChild(teamCard);
});
} }
// 渲染积分榜 // 渲染积分榜
function renderStandings() { function renderStandings() {
const standingsTable = document.querySelector('.standings-table tbody'); const standingsTable = document.querySelector(".standings-table tbody");
if (!standingsTable) return; if (!standingsTable) return;
standingsTable.innerHTML = ''; standingsTable.innerHTML = "";
leagueData.standings.forEach(standing => { leagueData.standings.forEach((standing) => {
const team = getTeamById(standing.teamId); const team = getTeamById(standing.teamId);
const row = document.createElement('tr'); const row = document.createElement("tr");
// 根据排名添加特殊样式 // 根据排名添加特殊样式
if (standing.rank <= 4) { if (standing.rank <= 4) {
row.classList.add('champions-league'); row.classList.add("champions-league");
} else if (standing.rank <= 6) { } else if (standing.rank <= 6) {
row.classList.add('europa-league'); row.classList.add("europa-league");
} else if (standing.rank >= 11) { } else if (standing.rank >= 11) {
row.classList.add('relegation'); row.classList.add("relegation");
} }
row.innerHTML = ` row.innerHTML = `
<td>${standing.rank}</td> <td>${standing.rank}</td>
<td> <td>
<div style="display: flex; align-items: center; gap: 0.5rem;"> <div style="display: flex; align-items: center; gap: 0.5rem;">
@ -224,70 +224,80 @@ function renderStandings() {
<td>${standing.lost}</td> <td>${standing.lost}</td>
<td>${standing.goalsFor}</td> <td>${standing.goalsFor}</td>
<td>${standing.goalsAgainst}</td> <td>${standing.goalsAgainst}</td>
<td>${standing.goalDifference > 0 ? '+' : ''}${standing.goalDifference}</td> <td>${standing.goalDifference > 0 ? "+" : ""}${standing.goalDifference}</td>
<td><strong>${standing.points}</strong></td> <td><strong>${standing.points}</strong></td>
`; `;
standingsTable.appendChild(row); standingsTable.appendChild(row);
}); });
} }
// 渲染赛程表 // 渲染赛程表
function renderFixtures() { function renderFixtures() {
const fixturesList = document.querySelector('.fixtures-list'); const fixturesList = document.querySelector(".fixtures-list");
if (!fixturesList) return; if (!fixturesList) return;
fixturesList.innerHTML = ''; fixturesList.innerHTML = "";
// 按轮次分组 // 按轮次分组
const fixturesByRound = {}; const fixturesByRound = {};
leagueData.fixtures.forEach(fixture => { leagueData.fixtures.forEach((fixture) => {
if (!fixturesByRound[fixture.round]) { if (!fixturesByRound[fixture.round]) {
fixturesByRound[fixture.round] = []; fixturesByRound[fixture.round] = [];
} }
fixturesByRound[fixture.round].push(fixture); fixturesByRound[fixture.round].push(fixture);
}); });
// 渲染所有赛程 // 渲染所有赛程
Object.keys(fixturesByRound).sort((a, b) => a - b).forEach(round => { Object.keys(fixturesByRound)
const roundHeader = document.createElement('div'); .sort((a, b) => a - b)
roundHeader.className = 'fixture-round-header'; .forEach((round) => {
roundHeader.innerHTML = `<h3>第${round}轮</h3>`; const roundHeader = document.createElement("div");
fixturesList.appendChild(roundHeader); roundHeader.className = "fixture-round-header";
roundHeader.innerHTML = `<h3>第${round}轮</h3>`;
fixturesByRound[round].forEach(fixture => { fixturesList.appendChild(roundHeader);
const homeTeam = getTeamById(fixture.homeTeamId);
const awayTeam = getTeamById(fixture.awayTeamId); fixturesByRound[round].forEach((fixture) => {
const homeTeam = getTeamById(fixture.homeTeamId);
const fixtureItem = document.createElement('div'); const awayTeam = getTeamById(fixture.awayTeamId);
fixtureItem.className = 'fixture-item';
const fixtureItem = document.createElement("div");
const date = new Date(fixture.date); fixtureItem.className = "fixture-item";
const dayNames = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
const dayName = dayNames[date.getDay()]; const date = new Date(fixture.date);
const dayNames = [
let scoreHtml = ''; "周日",
let statusText = ''; "周一",
"周二",
if (fixture.status === 'completed') { "周三",
scoreHtml = ` "周四",
"周五",
"周六",
];
const dayName = dayNames[date.getDay()];
let scoreHtml = "";
let statusText = "";
if (fixture.status === "completed") {
scoreHtml = `
<div class="fixture-score-value">${fixture.homeScore} - ${fixture.awayScore}</div> <div class="fixture-score-value">${fixture.homeScore} - ${fixture.awayScore}</div>
<div class="fixture-score-status">已结束</div> <div class="fixture-score-status">已结束</div>
`; `;
} else if (fixture.status === 'scheduled') { } else if (fixture.status === "scheduled") {
scoreHtml = ` scoreHtml = `
<div class="fixture-score-value">VS</div> <div class="fixture-score-value">VS</div>
<div class="fixture-score-status">${fixture.time}</div> <div class="fixture-score-status">${fixture.time}</div>
`; `;
} else { } else {
scoreHtml = ` scoreHtml = `
<div class="fixture-score-value">-</div> <div class="fixture-score-value">-</div>
<div class="fixture-score-status">待定</div> <div class="fixture-score-status">待定</div>
`; `;
} }
fixtureItem.innerHTML = ` fixtureItem.innerHTML = `
<div class="fixture-date"> <div class="fixture-date">
<div class="fixture-day">${dayName}</div> <div class="fixture-day">${dayName}</div>
<div class="fixture-time">${formatDate(fixture.date)}</div> <div class="fixture-time">${formatDate(fixture.date)}</div>
@ -307,25 +317,25 @@ function renderFixtures() {
${scoreHtml} ${scoreHtml}
</div> </div>
`; `;
fixturesList.appendChild(fixtureItem); fixturesList.appendChild(fixtureItem);
}); });
}); });
} }
// 渲染数据统计 // 渲染数据统计
function renderStats() { function renderStats() {
renderScorers(); renderScorers();
renderAssists(); renderAssists();
renderTeamStats(); renderTeamStats();
} }
function renderScorers() { function renderScorers() {
const scorersContainer = document.querySelector('#scorers'); const scorersContainer = document.querySelector("#scorers");
if (!scorersContainer) return; if (!scorersContainer) return;
scorersContainer.innerHTML = ` scorersContainer.innerHTML = `
<table class="stats-table"> <table class="stats-table">
<thead> <thead>
<tr> <tr>
@ -338,7 +348,8 @@ function renderScorers() {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
${leagueData.players.scorers.map(player => { ${leagueData.players.scorers
.map((player) => {
const team = getTeamById(player.teamId); const team = getTeamById(player.teamId);
return ` return `
<tr> <tr>
@ -350,18 +361,19 @@ function renderScorers() {
<td class="stats-value">${player.matches}</td> <td class="stats-value">${player.matches}</td>
</tr> </tr>
`; `;
}).join('')} })
.join("")}
</tbody> </tbody>
</table> </table>
`; `;
} }
function renderAssists() { function renderAssists() {
const assistsContainer = document.querySelector('#assists'); const assistsContainer = document.querySelector("#assists");
if (!assistsContainer) return; if (!assistsContainer) return;
assistsContainer.innerHTML = ` assistsContainer.innerHTML = `
<table class="stats-table"> <table class="stats-table">
<thead> <thead>
<tr> <tr>
@ -374,7 +386,8 @@ function renderAssists() {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
${leagueData.players.assists.map(player => { ${leagueData.players.assists
.map((player) => {
const team = getTeamById(player.teamId); const team = getTeamById(player.teamId);
return ` return `
<tr> <tr>
@ -386,36 +399,41 @@ function renderAssists() {
<td class="stats-value">${player.matches}</td> <td class="stats-value">${player.matches}</td>
</tr> </tr>
`; `;
}).join('')} })
.join("")}
</tbody> </tbody>
</table> </table>
`; `;
} }
function renderTeamStats() { function renderTeamStats() {
const teamStatsContainer = document.querySelector('#teams'); const teamStatsContainer = document.querySelector("#teams");
if (!teamStatsContainer) return; if (!teamStatsContainer) return;
// 计算球队统计数据 // 计算球队统计数据
const teamStats = leagueData.standings.map(standing => { const teamStats = leagueData.standings
const team = getTeamById(standing.teamId); .map((standing) => {
const goalsPerGame = (standing.goalsFor / standing.played).toFixed(2); const team = getTeamById(standing.teamId);
const concededPerGame = (standing.goalsAgainst / standing.played).toFixed(2); const goalsPerGame = (standing.goalsFor / standing.played).toFixed(2);
const concededPerGame = (standing.goalsAgainst / standing.played).toFixed(
return { 2,
rank: standing.rank, );
team: team.name,
goalsFor: standing.goalsFor, return {
goalsAgainst: standing.goalsAgainst, rank: standing.rank,
goalDifference: standing.goalDifference, team: team.name,
goalsPerGame, goalsFor: standing.goalsFor,
concededPerGame, goalsAgainst: standing.goalsAgainst,
cleanSheets: Math.floor(Math.random() * 5) // 模拟数据 goalDifference: standing.goalDifference,
}; goalsPerGame,
}).sort((a, b) => a.rank - b.rank); concededPerGame,
cleanSheets: Math.floor(Math.random() * 5), // 模拟数据
teamStatsContainer.innerHTML = ` };
})
.sort((a, b) => a.rank - b.rank);
teamStatsContainer.innerHTML = `
<table class="stats-table"> <table class="stats-table">
<thead> <thead>
<tr> <tr>
@ -430,18 +448,22 @@ function renderTeamStats() {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
${teamStats.map(stat => ` ${teamStats
.map(
(stat) => `
<tr> <tr>
<td class="stats-rank">${stat.rank}</td> <td class="stats-rank">${stat.rank}</td>
<td class="stats-player">${stat.team}</td> <td class="stats-player">${stat.team}</td>
<td class="stats-value">${stat.goalsFor}</td> <td class="stats-value">${stat.goalsFor}</td>
<td class="stats-value">${stat.goalsAgainst}</td> <td class="stats-value">${stat.goalsAgainst}</td>
<td class="stats-value">${stat.goalDifference > 0 ? '+' : ''}${stat.goalDifference}</td> <td class="stats-value">${stat.goalDifference > 0 ? "+" : ""}${stat.goalDifference}</td>
<td class="stats-value">${stat.goalsPerGame}</td> <td class="stats-value">${stat.goalsPerGame}</td>
<td class="stats-value">${stat.concededPerGame}</td> <td class="stats-value">${stat.concededPerGame}</td>
<td class="stats-value">${stat.cleanSheets}</td> <td class="stats-value">${stat.cleanSheets}</td>
</tr> </tr>
`).join('')} `,
)
.join("")}
</tbody> </tbody>
</table> </table>
`; `;
@ -449,24 +471,24 @@ function renderTeamStats() {
// 渲染新闻动态 // 渲染新闻动态
function renderNews() { function renderNews() {
const newsGrid = document.querySelector('.news-grid'); const newsGrid = document.querySelector(".news-grid");
if (!newsGrid) return; if (!newsGrid) return;
newsGrid.innerHTML = ''; newsGrid.innerHTML = "";
leagueData.news.forEach(newsItem => { leagueData.news.forEach((newsItem) => {
const newsCard = document.createElement('div'); const newsCard = document.createElement("div");
newsCard.className = 'news-card'; newsCard.className = "news-card";
const date = new Date(newsItem.date); const date = new Date(newsItem.date);
const formattedDate = date.toLocaleDateString('zh-CN', { const formattedDate = date.toLocaleDateString("zh-CN", {
year: 'numeric', year: "numeric",
month: 'long', month: "long",
day: 'numeric' day: "numeric",
}); });
newsCard.innerHTML = ` newsCard.innerHTML = `
<div class="news-card-image" style="background: linear-gradient(135deg, ${newsItem.imageColor} 0%, ${darkenColor(newsItem.imageColor, 20)} 100%);"></div> <div class="news-card-image" style="background: linear-gradient(135deg, ${newsItem.imageColor} 0%, ${darkenColor(newsItem.imageColor, 20)} 100%);"></div>
<div class="news-card-content"> <div class="news-card-content">
<span class="news-card-category">${newsItem.category}</span> <span class="news-card-category">${newsItem.category}</span>
@ -481,138 +503,143 @@ function renderNews() {
</div> </div>
</div> </div>
`; `;
newsCard.addEventListener('click', () => { newsCard.addEventListener("click", () => {
alert(`查看新闻: ${newsItem.title}`); alert(`查看新闻: ${newsItem.title}`);
});
newsGrid.appendChild(newsCard);
}); });
newsGrid.appendChild(newsCard);
});
} }
// 初始化标签页切换 // 初始化标签页切换
function initTabs() { function initTabs() {
// 赛程标签页 // 赛程标签页
const fixtureTabs = document.querySelectorAll('.fixtures-tabs .tab'); const fixtureTabs = document.querySelectorAll(".fixtures-tabs .tab");
const fixtureItems = document.querySelectorAll('.fixture-item'); const fixtureItems = document.querySelectorAll(".fixture-item");
fixtureTabs.forEach(tab => { fixtureTabs.forEach((tab) => {
tab.addEventListener('click', () => { tab.addEventListener("click", () => {
// 更新活动标签 // 更新活动标签
fixtureTabs.forEach(t => t.classList.remove('active')); fixtureTabs.forEach((t) => t.classList.remove("active"));
tab.classList.add('active'); tab.classList.add("active");
const roundFilter = tab.getAttribute('data-round'); const roundFilter = tab.getAttribute("data-round");
// 这里可以根据筛选条件显示不同的赛程 // 这里可以根据筛选条件显示不同的赛程
// 由于时间关系,这里只是简单的演示 // 由于时间关系,这里只是简单的演示
console.log(`筛选赛程: ${roundFilter}`); console.log(`筛选赛程: ${roundFilter}`);
});
}); });
});
// 数据统计标签页
const statsTabs = document.querySelectorAll('.stats-tab'); // 数据统计标签页
const statsContents = document.querySelectorAll('.stats-tab-content'); const statsTabs = document.querySelectorAll(".stats-tab");
const statsContents = document.querySelectorAll(".stats-tab-content");
statsTabs.forEach(tab => {
tab.addEventListener('click', () => { statsTabs.forEach((tab) => {
const tabId = tab.getAttribute('data-tab'); tab.addEventListener("click", () => {
const tabId = tab.getAttribute("data-tab");
// 更新活动标签
statsTabs.forEach(t => t.classList.remove('active')); // 更新活动标签
tab.classList.add('active'); statsTabs.forEach((t) => t.classList.remove("active"));
tab.classList.add("active");
// 显示对应内容
statsContents.forEach(content => { // 显示对应内容
content.classList.remove('active'); statsContents.forEach((content) => {
if (content.id === tabId) { content.classList.remove("active");
content.classList.add('active'); if (content.id === tabId) {
} content.classList.add("active");
}); }
}); });
}); });
});
} }
// 初始化移动端菜单 // 初始化移动端菜单
function initMobileMenu() { function initMobileMenu() {
const menuToggle = document.querySelector('.btn-menu-toggle'); const menuToggle = document.querySelector(".btn-menu-toggle");
const navMenu = document.querySelector('.nav-menu'); const navMenu = document.querySelector(".nav-menu");
if (menuToggle && navMenu) { if (menuToggle && navMenu) {
menuToggle.addEventListener('click', () => { menuToggle.addEventListener("click", () => {
navMenu.classList.toggle('active'); navMenu.classList.toggle("active");
// 更新菜单图标 // 更新菜单图标
const icon = menuToggle.querySelector('i'); const icon = menuToggle.querySelector("i");
if (navMenu.classList.contains('active')) { if (navMenu.classList.contains("active")) {
icon.className = 'fas fa-times'; icon.className = "fas fa-times";
} else { } else {
icon.className = 'fas fa-bars'; icon.className = "fas fa-bars";
} }
}); });
// 点击菜单外区域关闭菜单 // 点击菜单外区域关闭菜单
document.addEventListener('click', (e) => { document.addEventListener("click", (e) => {
if (!navMenu.contains(e.target) && !menuToggle.contains(e.target)) { if (!navMenu.contains(e.target) && !menuToggle.contains(e.target)) {
navMenu.classList.remove('active'); navMenu.classList.remove("active");
menuToggle.querySelector('i').className = 'fas fa-bars'; menuToggle.querySelector("i").className = "fas fa-bars";
} }
}); });
} }
} }
// 工具函数:加深颜色 // 工具函数:加深颜色
function darkenColor(color, percent) { function darkenColor(color, percent) {
const num = parseInt(color.replace("#", ""), 16); const num = parseInt(color.replace("#", ""), 16);
const amt = Math.round(2.55 * percent); const amt = Math.round(2.55 * percent);
const R = (num >> 16) - amt; const R = (num >> 16) - amt;
const G = (num >> 8 & 0x00FF) - amt; const G = ((num >> 8) & 0x00ff) - amt;
const B = (num & 0x0000FF) - amt; const B = (num & 0x0000ff) - amt;
return "#" + ( return (
0x1000000 + "#" +
(R < 255 ? R < 1 ? 0 : R : 255) * 0x10000 + (
(G < 255 ? G < 1 ? 0 : G : 255) * 0x100 + 0x1000000 +
(B < 255 ? B < 1 ? 0 : B : 255) (R < 255 ? (R < 1 ? 0 : R) : 255) * 0x10000 +
).toString(16).slice(1); (G < 255 ? (G < 1 ? 0 : G) : 255) * 0x100 +
(B < 255 ? (B < 1 ? 0 : B) : 255)
)
.toString(16)
.slice(1)
);
} }
// 工具函数:格式化日期(简写) // 工具函数:格式化日期(简写)
function formatDate(dateString) { function formatDate(dateString) {
const date = new Date(dateString); const date = new Date(dateString);
const month = date.getMonth() + 1; const month = date.getMonth() + 1;
const day = date.getDate(); const day = date.getDate();
return `${month}${day}`; return `${month}${day}`;
} }
// 工具函数根据ID获取球队信息 // 工具函数根据ID获取球队信息
function getTeamById(teamId) { function getTeamById(teamId) {
return leagueData.teams.find(team => team.id === teamId); return leagueData.teams.find((team) => team.id === teamId);
} }
// 添加一些交互效果 // 添加一些交互效果
document.addEventListener('DOMContentLoaded', () => { document.addEventListener("DOMContentLoaded", () => {
// 为所有按钮添加点击效果 // 为所有按钮添加点击效果
const buttons = document.querySelectorAll('.btn'); const buttons = document.querySelectorAll(".btn");
buttons.forEach(button => { buttons.forEach((button) => {
button.addEventListener('mousedown', () => { button.addEventListener("mousedown", () => {
button.style.transform = 'scale(0.95)'; button.style.transform = "scale(0.95)";
});
button.addEventListener('mouseup', () => {
button.style.transform = '';
});
button.addEventListener('mouseleave', () => {
button.style.transform = '';
});
}); });
// 为卡片添加悬停效果 button.addEventListener("mouseup", () => {
const cards = document.querySelectorAll('.team-card, .news-card'); button.style.transform = "";
cards.forEach(card => {
card.addEventListener('mouseenter', () => {
card.style.transition = 'transform 0.3s ease, box-shadow 0.3s ease';
});
}); });
});
button.addEventListener("mouseleave", () => {
button.style.transform = "";
});
});
// 为卡片添加悬停效果
const cards = document.querySelectorAll(".team-card, .news-card");
cards.forEach((card) => {
card.addEventListener("mouseenter", () => {
card.style.transition = "transform 0.3s ease, box-shadow 0.3s ease";
});
});
});

View File

@ -1,385 +1,533 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>2026 Horizons: Trends & Opportunities</title> <title>2026 Horizons: Trends & Opportunities</title>
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Space+Grotesk:wght@400;500;600&display=swap" rel="stylesheet"> <link
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Space+Grotesk:wght@400;500;600&display=swap"
<link rel="stylesheet" href="style.css"> rel="stylesheet"
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>📈</text></svg>"> />
</head> <link
<body> rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
/>
<link rel="stylesheet" href="style.css" />
<link
rel="icon"
type="image/svg+xml"
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>📈</text></svg>"
/>
</head>
<body>
<!-- Navigation --> <!-- Navigation -->
<nav class="navbar"> <nav class="navbar">
<div class="container"> <div class="container">
<div class="nav-brand"> <div class="nav-brand">
<span class="brand-icon">📈</span> <span class="brand-icon">📈</span>
<span class="brand-text">2026 Horizons</span> <span class="brand-text">2026 Horizons</span>
</div>
<ul class="nav-links">
<li><a href="#overview">Overview</a></li>
<li><a href="#trends">Trends</a></li>
<li><a href="#opportunities">Opportunities</a></li>
<li><a href="#challenges">Challenges</a></li>
</ul>
<button class="theme-toggle" id="themeToggle">
<i class="fas fa-moon"></i>
</button>
</div> </div>
<ul class="nav-links">
<li><a href="#overview">Overview</a></li>
<li><a href="#trends">Trends</a></li>
<li><a href="#opportunities">Opportunities</a></li>
<li><a href="#challenges">Challenges</a></li>
</ul>
<button class="theme-toggle" id="themeToggle">
<i class="fas fa-moon"></i>
</button>
</div>
</nav> </nav>
<!-- Hero Section --> <!-- Hero Section -->
<header class="hero"> <header class="hero">
<div class="container"> <div class="container">
<div class="hero-content"> <div class="hero-content">
<h1 class="hero-title">Navigating the Future</h1> <h1 class="hero-title">Navigating the Future</h1>
<p class="hero-subtitle">A comprehensive analysis of trends, opportunities, and challenges shaping 2026</p> <p class="hero-subtitle">
<div class="hero-stats"> A comprehensive analysis of trends, opportunities, and challenges
<div class="stat"> shaping 2026
<span class="stat-number">5</span> </p>
<span class="stat-label">Key Economic Trends</span> <div class="hero-stats">
</div> <div class="stat">
<div class="stat"> <span class="stat-number">5</span>
<span class="stat-number">8</span> <span class="stat-label">Key Economic Trends</span>
<span class="stat-label">High-Growth Markets</span>
</div>
<div class="stat">
<span class="stat-number">4</span>
<span class="stat-label">Technology Shifts</span>
</div>
</div>
<a href="#trends" class="cta-button">Explore Trends <i class="fas fa-arrow-down"></i></a>
</div> </div>
<div class="hero-visual"> <div class="stat">
<div class="visual-element"> <span class="stat-number">8</span>
<div class="circle"></div> <span class="stat-label">High-Growth Markets</span>
<div class="line"></div>
<div class="dot"></div>
</div>
</div> </div>
<div class="stat">
<span class="stat-number">4</span>
<span class="stat-label">Technology Shifts</span>
</div>
</div>
<a href="#trends" class="cta-button"
>Explore Trends <i class="fas fa-arrow-down"></i
></a>
</div> </div>
<div class="hero-visual">
<div class="visual-element">
<div class="circle"></div>
<div class="line"></div>
<div class="dot"></div>
</div>
</div>
</div>
</header> </header>
<!-- Overview Section --> <!-- Overview Section -->
<section class="section overview" id="overview"> <section class="section overview" id="overview">
<div class="container"> <div class="container">
<div class="section-header"> <div class="section-header">
<h2 class="section-title">The 2026 Landscape</h2> <h2 class="section-title">The 2026 Landscape</h2>
<p class="section-subtitle">Convergence, complexity, and unprecedented opportunities</p> <p class="section-subtitle">
</div> Convergence, complexity, and unprecedented opportunities
<div class="overview-content"> </p>
<div class="overview-text">
<p>2026 represents a pivotal inflection point where accelerating technological convergence meets economic realignment and emerging market opportunities. The year will be defined by the interplay of AI maturation, quantum computing practicality, and sustainable transformation.</p>
<p>Organizations and individuals who can navigate this complexity while maintaining strategic agility will be best positioned to capitalize on emerging opportunities across technology, business, and sustainability sectors.</p>
</div>
<div class="overview-highlight">
<div class="highlight-card">
<div class="highlight-icon">
<i class="fas fa-brain"></i>
</div>
<h3 class="highlight-title">AI Maturation</h3>
<p class="highlight-text">Transition from experimentation to production deployment with autonomous agents</p>
</div>
<div class="highlight-card">
<div class="highlight-icon">
<i class="fas fa-leaf"></i>
</div>
<h3 class="highlight-title">Sustainability Focus</h3>
<p class="highlight-text">Climate tech emerges as a dominant investment category with material financial implications</p>
</div>
</div>
</div>
</div> </div>
<div class="overview-content">
<div class="overview-text">
<p>
2026 represents a pivotal inflection point where accelerating
technological convergence meets economic realignment and emerging
market opportunities. The year will be defined by the interplay of
AI maturation, quantum computing practicality, and sustainable
transformation.
</p>
<p>
Organizations and individuals who can navigate this complexity
while maintaining strategic agility will be best positioned to
capitalize on emerging opportunities across technology, business,
and sustainability sectors.
</p>
</div>
<div class="overview-highlight">
<div class="highlight-card">
<div class="highlight-icon">
<i class="fas fa-brain"></i>
</div>
<h3 class="highlight-title">AI Maturation</h3>
<p class="highlight-text">
Transition from experimentation to production deployment with
autonomous agents
</p>
</div>
<div class="highlight-card">
<div class="highlight-icon">
<i class="fas fa-leaf"></i>
</div>
<h3 class="highlight-title">Sustainability Focus</h3>
<p class="highlight-text">
Climate tech emerges as a dominant investment category with
material financial implications
</p>
</div>
</div>
</div>
</div>
</section> </section>
<!-- Trends Section --> <!-- Trends Section -->
<section class="section trends" id="trends"> <section class="section trends" id="trends">
<div class="container"> <div class="container">
<div class="section-header"> <div class="section-header">
<h2 class="section-title">Key Trends Shaping 2026</h2> <h2 class="section-title">Key Trends Shaping 2026</h2>
<p class="section-subtitle">Critical developments across technology, economy, and society</p> <p class="section-subtitle">
</div> Critical developments across technology, economy, and society
</p>
<div class="trends-grid">
<!-- Technology Trends -->
<div class="trend-category">
<h3 class="category-title"><i class="fas fa-microchip"></i> Technology & Innovation</h3>
<div class="trend-cards">
<div class="trend-card">
<div class="trend-header">
<span class="trend-badge tech">AI</span>
<span class="trend-priority high">High Impact</span>
</div>
<h4 class="trend-name">AI Agents Proliferation</h4>
<p class="trend-description">Autonomous AI agents become mainstream in enterprise operations, requiring sophisticated governance frameworks and security considerations.</p>
<div class="trend-metrics">
<span class="metric"><i class="fas fa-rocket"></i> Exponential Growth</span>
<span class="metric"><i class="fas fa-shield-alt"></i> Security Critical</span>
</div>
</div>
<div class="trend-card">
<div class="trend-header">
<span class="trend-badge tech">Quantum</span>
<span class="trend-priority medium">Emerging</span>
</div>
<h4 class="trend-name">Quantum-AI Convergence</h4>
<p class="trend-description">18% of global quantum algorithm revenues expected from AI applications, marking a significant shift toward practical quantum computing applications.</p>
<div class="trend-metrics">
<span class="metric"><i class="fas fa-chart-line"></i> 18% Revenue Share</span>
<span class="metric"><i class="fas fa-cogs"></i> Optimization Focus</span>
</div>
</div>
<div class="trend-card">
<div class="trend-header">
<span class="trend-badge tech">Security</span>
<span class="trend-priority high">Critical</span>
</div>
<h4 class="trend-name">AI-Powered Cybersecurity</h4>
<p class="trend-description">Organizations leverage AI for threat detection, red teaming, and automated defense at machine speed, creating new security paradigms.</p>
<div class="trend-metrics">
<span class="metric"><i class="fas fa-bolt"></i> Machine Speed</span>
<span class="metric"><i class="fas fa-user-shield"></i> Proactive Defense</span>
</div>
</div>
</div>
</div>
<!-- Economic Trends -->
<div class="trend-category">
<h3 class="category-title"><i class="fas fa-chart-line"></i> Economic & Global</h3>
<div class="trend-cards">
<div class="trend-card">
<div class="trend-header">
<span class="trend-badge econ">Finance</span>
<span class="trend-priority high">Transformative</span>
</div>
<h4 class="trend-name">Tokenized Cross-Border Payments</h4>
<p class="trend-description">Nearly 75% of G20 countries expected to have digital token payment systems, challenging traditional banking and dollar dominance.</p>
<div class="trend-metrics">
<span class="metric"><i class="fas fa-globe"></i> 75% G20 Adoption</span>
<span class="metric"><i class="fas fa-exchange-alt"></i> Borderless</span>
</div>
</div>
<div class="trend-card">
<div class="trend-header">
<span class="trend-badge econ">Trade</span>
<span class="trend-priority medium">Volatile</span>
</div>
<h4 class="trend-name">Trade Realignments</h4>
<p class="trend-description">Continued US-China tensions with potential EU tariff responses on advanced manufacturing, reshaping global supply chains.</p>
<div class="trend-metrics">
<span class="metric"><i class="fas fa-balance-scale"></i> Geopolitical Shift</span>
<span class="metric"><i class="fas fa-industry"></i> Supply Chain Impact</span>
</div>
</div>
<div class="trend-card">
<div class="trend-header">
<span class="trend-badge econ">Risk</span>
<span class="trend-priority high">Critical</span>
</div>
<h4 class="trend-name">Debt Sustainability Challenges</h4>
<p class="trend-description">Record public debt levels with limited fiscal restraint appetite as central banks unwind balance sheets.</p>
<div class="trend-metrics">
<span class="metric"><i class="fas fa-exclamation-triangle"></i> Record Levels</span>
<span class="metric"><i class="fas fa-percentage"></i> Yield Pressure</span>
</div>
</div>
</div>
</div>
</div>
</div> </div>
<div class="trends-grid">
<!-- Technology Trends -->
<div class="trend-category">
<h3 class="category-title">
<i class="fas fa-microchip"></i> Technology & Innovation
</h3>
<div class="trend-cards">
<div class="trend-card">
<div class="trend-header">
<span class="trend-badge tech">AI</span>
<span class="trend-priority high">High Impact</span>
</div>
<h4 class="trend-name">AI Agents Proliferation</h4>
<p class="trend-description">
Autonomous AI agents become mainstream in enterprise
operations, requiring sophisticated governance frameworks and
security considerations.
</p>
<div class="trend-metrics">
<span class="metric"
><i class="fas fa-rocket"></i> Exponential Growth</span
>
<span class="metric"
><i class="fas fa-shield-alt"></i> Security Critical</span
>
</div>
</div>
<div class="trend-card">
<div class="trend-header">
<span class="trend-badge tech">Quantum</span>
<span class="trend-priority medium">Emerging</span>
</div>
<h4 class="trend-name">Quantum-AI Convergence</h4>
<p class="trend-description">
18% of global quantum algorithm revenues expected from AI
applications, marking a significant shift toward practical
quantum computing applications.
</p>
<div class="trend-metrics">
<span class="metric"
><i class="fas fa-chart-line"></i> 18% Revenue Share</span
>
<span class="metric"
><i class="fas fa-cogs"></i> Optimization Focus</span
>
</div>
</div>
<div class="trend-card">
<div class="trend-header">
<span class="trend-badge tech">Security</span>
<span class="trend-priority high">Critical</span>
</div>
<h4 class="trend-name">AI-Powered Cybersecurity</h4>
<p class="trend-description">
Organizations leverage AI for threat detection, red teaming,
and automated defense at machine speed, creating new security
paradigms.
</p>
<div class="trend-metrics">
<span class="metric"
><i class="fas fa-bolt"></i> Machine Speed</span
>
<span class="metric"
><i class="fas fa-user-shield"></i> Proactive Defense</span
>
</div>
</div>
</div>
</div>
<!-- Economic Trends -->
<div class="trend-category">
<h3 class="category-title">
<i class="fas fa-chart-line"></i> Economic & Global
</h3>
<div class="trend-cards">
<div class="trend-card">
<div class="trend-header">
<span class="trend-badge econ">Finance</span>
<span class="trend-priority high">Transformative</span>
</div>
<h4 class="trend-name">Tokenized Cross-Border Payments</h4>
<p class="trend-description">
Nearly 75% of G20 countries expected to have digital token
payment systems, challenging traditional banking and dollar
dominance.
</p>
<div class="trend-metrics">
<span class="metric"
><i class="fas fa-globe"></i> 75% G20 Adoption</span
>
<span class="metric"
><i class="fas fa-exchange-alt"></i> Borderless</span
>
</div>
</div>
<div class="trend-card">
<div class="trend-header">
<span class="trend-badge econ">Trade</span>
<span class="trend-priority medium">Volatile</span>
</div>
<h4 class="trend-name">Trade Realignments</h4>
<p class="trend-description">
Continued US-China tensions with potential EU tariff responses
on advanced manufacturing, reshaping global supply chains.
</p>
<div class="trend-metrics">
<span class="metric"
><i class="fas fa-balance-scale"></i> Geopolitical
Shift</span
>
<span class="metric"
><i class="fas fa-industry"></i> Supply Chain Impact</span
>
</div>
</div>
<div class="trend-card">
<div class="trend-header">
<span class="trend-badge econ">Risk</span>
<span class="trend-priority high">Critical</span>
</div>
<h4 class="trend-name">Debt Sustainability Challenges</h4>
<p class="trend-description">
Record public debt levels with limited fiscal restraint
appetite as central banks unwind balance sheets.
</p>
<div class="trend-metrics">
<span class="metric"
><i class="fas fa-exclamation-triangle"></i> Record
Levels</span
>
<span class="metric"
><i class="fas fa-percentage"></i> Yield Pressure</span
>
</div>
</div>
</div>
</div>
</div>
</div>
</section> </section>
<!-- Opportunities Section --> <!-- Opportunities Section -->
<section class="section opportunities" id="opportunities"> <section class="section opportunities" id="opportunities">
<div class="container"> <div class="container">
<div class="section-header"> <div class="section-header">
<h2 class="section-title">Emerging Opportunities</h2> <h2 class="section-title">Emerging Opportunities</h2>
<p class="section-subtitle">High-growth markets and strategic investment areas</p> <p class="section-subtitle">
</div> High-growth markets and strategic investment areas
</p>
<div class="opportunities-grid">
<div class="opportunity-card">
<div class="opportunity-icon climate">
<i class="fas fa-solar-panel"></i>
</div>
<h3 class="opportunity-title">Climate Technology</h3>
<p class="opportunity-description">Home energy solutions, carbon capture, and sustainable infrastructure with massive growth potential.</p>
<div class="opportunity-market">
<span class="market-size">$162B+</span>
<span class="market-label">by 2030</span>
</div>
</div>
<div class="opportunity-card">
<div class="opportunity-icon health">
<i class="fas fa-heartbeat"></i>
</div>
<h3 class="opportunity-title">Preventive Health</h3>
<p class="opportunity-description">Personalized wellness, early intervention technologies, and digital health platforms.</p>
<div class="opportunity-market">
<span class="market-size">High Growth</span>
<span class="market-label">Post-pandemic focus</span>
</div>
</div>
<div class="opportunity-card">
<div class="opportunity-icon tech">
<i class="fas fa-robot"></i>
</div>
<h3 class="opportunity-title">AI Consulting</h3>
<p class="opportunity-description">Industry-specific AI implementation services and agentic AI platform development.</p>
<div class="opportunity-market">
<span class="market-size">Specialized</span>
<span class="market-label">Enterprise demand</span>
</div>
</div>
<div class="opportunity-card">
<div class="opportunity-icon food">
<i class="fas fa-seedling"></i>
</div>
<h3 class="opportunity-title">Plant-Based Foods</h3>
<p class="opportunity-description">Sustainable food alternatives with projected market growth toward $162 billion by 2030.</p>
<div class="opportunity-market">
<span class="market-size">$162B</span>
<span class="market-label">Market potential</span>
</div>
</div>
</div>
<div class="opportunity-highlight">
<div class="highlight-content">
<h3 class="highlight-title">Strategic Investment Shift</h3>
<p>Venture capital is diversifying geographically with emerging hubs in Lagos, Bucharest, Riyadh, and other non-traditional locations. Decentralized finance continues to innovate alternatives to traditional systems.</p>
</div>
<div class="highlight-stats">
<div class="stat-item">
<span class="stat-value">75%</span>
<span class="stat-label">G20 Digital Payments</span>
</div>
<div class="stat-item">
<span class="stat-value">18%</span>
<span class="stat-label">Quantum-AI Revenue</span>
</div>
</div>
</div>
</div> </div>
<div class="opportunities-grid">
<div class="opportunity-card">
<div class="opportunity-icon climate">
<i class="fas fa-solar-panel"></i>
</div>
<h3 class="opportunity-title">Climate Technology</h3>
<p class="opportunity-description">
Home energy solutions, carbon capture, and sustainable
infrastructure with massive growth potential.
</p>
<div class="opportunity-market">
<span class="market-size">$162B+</span>
<span class="market-label">by 2030</span>
</div>
</div>
<div class="opportunity-card">
<div class="opportunity-icon health">
<i class="fas fa-heartbeat"></i>
</div>
<h3 class="opportunity-title">Preventive Health</h3>
<p class="opportunity-description">
Personalized wellness, early intervention technologies, and
digital health platforms.
</p>
<div class="opportunity-market">
<span class="market-size">High Growth</span>
<span class="market-label">Post-pandemic focus</span>
</div>
</div>
<div class="opportunity-card">
<div class="opportunity-icon tech">
<i class="fas fa-robot"></i>
</div>
<h3 class="opportunity-title">AI Consulting</h3>
<p class="opportunity-description">
Industry-specific AI implementation services and agentic AI
platform development.
</p>
<div class="opportunity-market">
<span class="market-size">Specialized</span>
<span class="market-label">Enterprise demand</span>
</div>
</div>
<div class="opportunity-card">
<div class="opportunity-icon food">
<i class="fas fa-seedling"></i>
</div>
<h3 class="opportunity-title">Plant-Based Foods</h3>
<p class="opportunity-description">
Sustainable food alternatives with projected market growth toward
$162 billion by 2030.
</p>
<div class="opportunity-market">
<span class="market-size">$162B</span>
<span class="market-label">Market potential</span>
</div>
</div>
</div>
<div class="opportunity-highlight">
<div class="highlight-content">
<h3 class="highlight-title">Strategic Investment Shift</h3>
<p>
Venture capital is diversifying geographically with emerging hubs
in Lagos, Bucharest, Riyadh, and other non-traditional locations.
Decentralized finance continues to innovate alternatives to
traditional systems.
</p>
</div>
<div class="highlight-stats">
<div class="stat-item">
<span class="stat-value">75%</span>
<span class="stat-label">G20 Digital Payments</span>
</div>
<div class="stat-item">
<span class="stat-value">18%</span>
<span class="stat-label">Quantum-AI Revenue</span>
</div>
</div>
</div>
</div>
</section> </section>
<!-- Challenges Section --> <!-- Challenges Section -->
<section class="section challenges" id="challenges"> <section class="section challenges" id="challenges">
<div class="container"> <div class="container">
<div class="section-header"> <div class="section-header">
<h2 class="section-title">Critical Challenges & Risks</h2> <h2 class="section-title">Critical Challenges & Risks</h2>
<p class="section-subtitle">Navigating complexity in an uncertain landscape</p> <p class="section-subtitle">
</div> Navigating complexity in an uncertain landscape
</p>
<div class="challenges-content">
<div class="challenge-card">
<div class="challenge-header">
<span class="challenge-severity high">High Risk</span>
<h3 class="challenge-title">AI Security Vulnerabilities</h3>
</div>
<p class="challenge-description">New attack vectors require comprehensive defense strategies as autonomous agents proliferate across organizations.</p>
<div class="challenge-mitigation">
<span class="mitigation-label">Mitigation:</span>
<span class="mitigation-text">Robust governance frameworks and AI-native security protocols</span>
</div>
</div>
<div class="challenge-card">
<div class="challenge-header">
<span class="challenge-severity medium">Medium Risk</span>
<h3 class="challenge-title">Talent & Skills Gap</h3>
</div>
<p class="challenge-description">Rapid technological change outpacing workforce skill development, creating critical talent shortages.</p>
<div class="challenge-mitigation">
<span class="mitigation-label">Mitigation:</span>
<span class="mitigation-text">Continuous upskilling programs and AI collaboration training</span>
</div>
</div>
<div class="challenge-card">
<div class="challenge-header">
<span class="challenge-severity high">High Risk</span>
<h3 class="challenge-title">Economic Volatility</h3>
</div>
<p class="challenge-description">Potential AI bubble concerns, trade fragmentation, and competing payment systems creating market uncertainty.</p>
<div class="challenge-mitigation">
<span class="mitigation-label">Mitigation:</span>
<span class="mitigation-text">Diversified portfolios and agile business models</span>
</div>
</div>
</div>
<div class="strategic-implications">
<h3 class="implications-title">Strategic Implications</h3>
<div class="implications-grid">
<div class="implication">
<h4>For Businesses</h4>
<p>Success requires embracing AI as a core competency while maintaining robust cybersecurity. Companies that navigate the sustainability transition while leveraging emerging technologies gain competitive advantages.</p>
</div>
<div class="implication">
<h4>For Investors</h4>
<p>Opportunities exist in climate tech, digital transformation, and Asian markets, but require careful assessment of geopolitical risks and potential market corrections.</p>
</div>
<div class="implication">
<h4>For Individuals</h4>
<p>Continuous upskilling in AI collaboration, quantum computing awareness, and digital literacy will be essential for career resilience in the evolving landscape.</p>
</div>
</div>
</div>
</div> </div>
<div class="challenges-content">
<div class="challenge-card">
<div class="challenge-header">
<span class="challenge-severity high">High Risk</span>
<h3 class="challenge-title">AI Security Vulnerabilities</h3>
</div>
<p class="challenge-description">
New attack vectors require comprehensive defense strategies as
autonomous agents proliferate across organizations.
</p>
<div class="challenge-mitigation">
<span class="mitigation-label">Mitigation:</span>
<span class="mitigation-text"
>Robust governance frameworks and AI-native security
protocols</span
>
</div>
</div>
<div class="challenge-card">
<div class="challenge-header">
<span class="challenge-severity medium">Medium Risk</span>
<h3 class="challenge-title">Talent & Skills Gap</h3>
</div>
<p class="challenge-description">
Rapid technological change outpacing workforce skill development,
creating critical talent shortages.
</p>
<div class="challenge-mitigation">
<span class="mitigation-label">Mitigation:</span>
<span class="mitigation-text"
>Continuous upskilling programs and AI collaboration
training</span
>
</div>
</div>
<div class="challenge-card">
<div class="challenge-header">
<span class="challenge-severity high">High Risk</span>
<h3 class="challenge-title">Economic Volatility</h3>
</div>
<p class="challenge-description">
Potential AI bubble concerns, trade fragmentation, and competing
payment systems creating market uncertainty.
</p>
<div class="challenge-mitigation">
<span class="mitigation-label">Mitigation:</span>
<span class="mitigation-text"
>Diversified portfolios and agile business models</span
>
</div>
</div>
</div>
<div class="strategic-implications">
<h3 class="implications-title">Strategic Implications</h3>
<div class="implications-grid">
<div class="implication">
<h4>For Businesses</h4>
<p>
Success requires embracing AI as a core competency while
maintaining robust cybersecurity. Companies that navigate the
sustainability transition while leveraging emerging technologies
gain competitive advantages.
</p>
</div>
<div class="implication">
<h4>For Investors</h4>
<p>
Opportunities exist in climate tech, digital transformation, and
Asian markets, but require careful assessment of geopolitical
risks and potential market corrections.
</p>
</div>
<div class="implication">
<h4>For Individuals</h4>
<p>
Continuous upskilling in AI collaboration, quantum computing
awareness, and digital literacy will be essential for career
resilience in the evolving landscape.
</p>
</div>
</div>
</div>
</div>
</section> </section>
<!-- Footer --> <!-- Footer -->
<footer class="footer"> <footer class="footer">
<div class="container"> <div class="container">
<div class="footer-content"> <div class="footer-content">
<div class="footer-brand"> <div class="footer-brand">
<span class="brand-icon">📈</span> <span class="brand-icon">📈</span>
<span class="brand-text">2026 Horizons</span> <span class="brand-text">2026 Horizons</span>
<p class="footer-description">An analysis of trends shaping the future landscape</p> <p class="footer-description">
</div> An analysis of trends shaping the future landscape
</p>
<div class="footer-links"> </div>
<div class="link-group">
<h4 class="link-title">Trends</h4> <div class="footer-links">
<a href="#trends">Technology</a> <div class="link-group">
<a href="#trends">Economic</a> <h4 class="link-title">Trends</h4>
<a href="#trends">Sustainability</a> <a href="#trends">Technology</a>
</div> <a href="#trends">Economic</a>
<div class="link-group"> <a href="#trends">Sustainability</a>
<h4 class="link-title">Opportunities</h4>
<a href="#opportunities">Markets</a>
<a href="#opportunities">Investments</a>
<a href="#opportunities">Startups</a>
</div>
</div>
</div> </div>
<div class="link-group">
<div class="footer-bottom"> <h4 class="link-title">Opportunities</h4>
<div class="copyright"> <a href="#opportunities">Markets</a>
<p>&copy; 2026 Horizons Analysis. Based on current research and expert predictions.</p> <a href="#opportunities">Investments</a>
</div> <a href="#opportunities">Startups</a>
<div class="deerflow-branding">
<a href="https://deerflow.tech" target="_blank" class="deerflow-link">
<span class="deerflow-icon"></span>
<span class="deerflow-text">Created by Deerflow</span>
</a>
</div>
</div> </div>
</div>
</div> </div>
<div class="footer-bottom">
<div class="copyright">
<p>
&copy; 2026 Horizons Analysis. Based on current research and
expert predictions.
</p>
</div>
<div class="deerflow-branding">
<a
href="https://deerflow.tech"
target="_blank"
class="deerflow-link"
>
<span class="deerflow-icon"></span>
<span class="deerflow-text">Created by Deerflow</span>
</a>
</div>
</div>
</div>
</footer> </footer>
<script src="script.js"></script> <script src="script.js"></script>
</body> </body>
</html> </html>

View File

@ -1,156 +1,168 @@
// 2026 Horizons - Interactive Features // 2026 Horizons - Interactive Features
document.addEventListener('DOMContentLoaded', function() { document.addEventListener("DOMContentLoaded", function () {
// Theme Toggle // Theme Toggle
const themeToggle = document.getElementById('themeToggle'); const themeToggle = document.getElementById("themeToggle");
const themeIcon = themeToggle.querySelector('i'); const themeIcon = themeToggle.querySelector("i");
// Check for saved theme or prefer-color-scheme // Check for saved theme or prefer-color-scheme
const savedTheme = localStorage.getItem('theme'); const savedTheme = localStorage.getItem("theme");
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
if (savedTheme === 'dark' || (!savedTheme && prefersDark)) { if (savedTheme === "dark" || (!savedTheme && prefersDark)) {
document.documentElement.setAttribute('data-theme', 'dark'); document.documentElement.setAttribute("data-theme", "dark");
themeIcon.className = 'fas fa-sun'; themeIcon.className = "fas fa-sun";
}
themeToggle.addEventListener("click", function () {
const currentTheme = document.documentElement.getAttribute("data-theme");
if (currentTheme === "dark") {
document.documentElement.removeAttribute("data-theme");
themeIcon.className = "fas fa-moon";
localStorage.setItem("theme", "light");
} else {
document.documentElement.setAttribute("data-theme", "dark");
themeIcon.className = "fas fa-sun";
localStorage.setItem("theme", "dark");
} }
});
themeToggle.addEventListener('click', function() {
const currentTheme = document.documentElement.getAttribute('data-theme'); // Smooth scroll for navigation links
document.querySelectorAll('a[href^="#"]').forEach((anchor) => {
if (currentTheme === 'dark') { anchor.addEventListener("click", function (e) {
document.documentElement.removeAttribute('data-theme'); e.preventDefault();
themeIcon.className = 'fas fa-moon';
localStorage.setItem('theme', 'light'); const targetId = this.getAttribute("href");
} else { if (targetId === "#") return;
document.documentElement.setAttribute('data-theme', 'dark');
themeIcon.className = 'fas fa-sun'; const targetElement = document.querySelector(targetId);
localStorage.setItem('theme', 'dark'); if (targetElement) {
} const headerHeight = document.querySelector(".navbar").offsetHeight;
}); const targetPosition = targetElement.offsetTop - headerHeight - 20;
// Smooth scroll for navigation links window.scrollTo({
document.querySelectorAll('a[href^="#"]').forEach(anchor => { top: targetPosition,
anchor.addEventListener('click', function(e) { behavior: "smooth",
e.preventDefault();
const targetId = this.getAttribute('href');
if (targetId === '#') return;
const targetElement = document.querySelector(targetId);
if (targetElement) {
const headerHeight = document.querySelector('.navbar').offsetHeight;
const targetPosition = targetElement.offsetTop - headerHeight - 20;
window.scrollTo({
top: targetPosition,
behavior: 'smooth'
});
}
}); });
}
}); });
});
// Navbar scroll effect
const navbar = document.querySelector('.navbar'); // Navbar scroll effect
let lastScrollTop = 0; const navbar = document.querySelector(".navbar");
let lastScrollTop = 0;
window.addEventListener('scroll', function() {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop; window.addEventListener("scroll", function () {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
// Hide/show navbar on scroll
if (scrollTop > lastScrollTop && scrollTop > 100) { // Hide/show navbar on scroll
navbar.style.transform = 'translateY(-100%)'; if (scrollTop > lastScrollTop && scrollTop > 100) {
} else { navbar.style.transform = "translateY(-100%)";
navbar.style.transform = 'translateY(0)'; } else {
} navbar.style.transform = "translateY(0)";
lastScrollTop = scrollTop;
// Add shadow when scrolled
if (scrollTop > 10) {
navbar.style.boxShadow = 'var(--shadow-md)';
} else {
navbar.style.boxShadow = 'none';
}
});
// Animate elements on scroll
const observerOptions = {
threshold: 0.1,
rootMargin: '0px 0px -50px 0px'
};
const observer = new IntersectionObserver(function(entries) {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('fade-in');
observer.unobserve(entry.target);
}
});
}, observerOptions);
// Observe elements to animate
document.querySelectorAll('.trend-card, .opportunity-card, .challenge-card, .highlight-card').forEach(el => {
observer.observe(el);
});
// Stats counter animation
const stats = document.querySelectorAll('.stat-number');
const statsObserver = new IntersectionObserver(function(entries) {
entries.forEach(entry => {
if (entry.isIntersecting) {
const stat = entry.target;
const targetValue = parseInt(stat.textContent);
let currentValue = 0;
const increment = targetValue / 50;
const duration = 1500;
const stepTime = Math.floor(duration / 50);
const timer = setInterval(() => {
currentValue += increment;
if (currentValue >= targetValue) {
stat.textContent = targetValue;
clearInterval(timer);
} else {
stat.textContent = Math.floor(currentValue);
}
}, stepTime);
statsObserver.unobserve(stat);
}
});
}, { threshold: 0.5 });
stats.forEach(stat => {
statsObserver.observe(stat);
});
// Hover effects for cards
document.querySelectorAll('.trend-card, .opportunity-card, .challenge-card').forEach(card => {
card.addEventListener('mouseenter', function() {
this.style.zIndex = '10';
});
card.addEventListener('mouseleave', function() {
this.style.zIndex = '1';
});
});
// Current year in footer
const currentYear = new Date().getFullYear();
const yearElement = document.querySelector('.copyright p');
if (yearElement) {
yearElement.textContent = yearElement.textContent.replace('2026', currentYear);
} }
// Initialize animations lastScrollTop = scrollTop;
setTimeout(() => {
document.body.style.opacity = '1'; // Add shadow when scrolled
}, 100); if (scrollTop > 10) {
navbar.style.boxShadow = "var(--shadow-md)";
} else {
navbar.style.boxShadow = "none";
}
});
// Animate elements on scroll
const observerOptions = {
threshold: 0.1,
rootMargin: "0px 0px -50px 0px",
};
const observer = new IntersectionObserver(function (entries) {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add("fade-in");
observer.unobserve(entry.target);
}
});
}, observerOptions);
// Observe elements to animate
document
.querySelectorAll(
".trend-card, .opportunity-card, .challenge-card, .highlight-card",
)
.forEach((el) => {
observer.observe(el);
});
// Stats counter animation
const stats = document.querySelectorAll(".stat-number");
const statsObserver = new IntersectionObserver(
function (entries) {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const stat = entry.target;
const targetValue = parseInt(stat.textContent);
let currentValue = 0;
const increment = targetValue / 50;
const duration = 1500;
const stepTime = Math.floor(duration / 50);
const timer = setInterval(() => {
currentValue += increment;
if (currentValue >= targetValue) {
stat.textContent = targetValue;
clearInterval(timer);
} else {
stat.textContent = Math.floor(currentValue);
}
}, stepTime);
statsObserver.unobserve(stat);
}
});
},
{ threshold: 0.5 },
);
stats.forEach((stat) => {
statsObserver.observe(stat);
});
// Hover effects for cards
document
.querySelectorAll(".trend-card, .opportunity-card, .challenge-card")
.forEach((card) => {
card.addEventListener("mouseenter", function () {
this.style.zIndex = "10";
});
card.addEventListener("mouseleave", function () {
this.style.zIndex = "1";
});
});
// Current year in footer
const currentYear = new Date().getFullYear();
const yearElement = document.querySelector(".copyright p");
if (yearElement) {
yearElement.textContent = yearElement.textContent.replace(
"2026",
currentYear,
);
}
// Initialize animations
setTimeout(() => {
document.body.style.opacity = "1";
}, 100);
}); });
// Add CSS for initial load // Add CSS for initial load
const style = document.createElement('style'); const style = document.createElement("style");
style.textContent = ` style.textContent = `
body { body {
opacity: 0; opacity: 0;
@ -172,4 +184,4 @@ style.textContent = `
} }
} }
`; `;
document.head.appendChild(style); document.head.appendChild(style);

View File

@ -1,6 +1,6 @@
# The Leica Master's Eye: Capturing the Decisive Moment in the Age of AI # The Leica Master's Eye: Capturing the Decisive Moment in the Age of AI
*By DeerFlow 2.0 | January 28, 2026* _By DeerFlow 2.0 | January 28, 2026_
## The Enduring Legacy of Leica Street Photography ## The Enduring Legacy of Leica Street Photography
@ -13,20 +13,25 @@ Through extensive research into Leica photography characteristics and careful pr
My research reveals several key characteristics that define Leica master photography: My research reveals several key characteristics that define Leica master photography:
### 1. The Decisive Moment Philosophy ### 1. The Decisive Moment Philosophy
Henri Cartier-Bresson famously described photography as "the simultaneous recognition, in a fraction of a second, of the significance of an event." This philosophy emphasizes perfect timing where all visual elements align to create meaning beyond the literal scene. Henri Cartier-Bresson famously described photography as "the simultaneous recognition, in a fraction of a second, of the significance of an event." This philosophy emphasizes perfect timing where all visual elements align to create meaning beyond the literal scene.
### 2. Rangefinder Discretion ### 2. Rangefinder Discretion
Leica's compact rangefinder design allows photographers to become part of the scene rather than observers behind bulky equipment. The quiet shutter and manual focus encourage deliberate, thoughtful composition. Leica's compact rangefinder design allows photographers to become part of the scene rather than observers behind bulky equipment. The quiet shutter and manual focus encourage deliberate, thoughtful composition.
### 3. Lens Character ### 3. Lens Character
Leica lenses are renowned for their "creamy bokeh" (background blur), natural color rendering, and three-dimensional "pop." Each lens has distinct characteristics—from the clinical sharpness of Summicron lenses to the dreamy quality of Noctilux wide-open. Leica lenses are renowned for their "creamy bokeh" (background blur), natural color rendering, and three-dimensional "pop." Each lens has distinct characteristics—from the clinical sharpness of Summicron lenses to the dreamy quality of Noctilux wide-open.
### 4. Film-Like Aesthetic ### 4. Film-Like Aesthetic
Even with digital Leicas, photographers often emulate film characteristics: natural grain, subtle color shifts, and a certain "organic" quality that avoids the sterile perfection of some digital photography. Even with digital Leicas, photographers often emulate film characteristics: natural grain, subtle color shifts, and a certain "organic" quality that avoids the sterile perfection of some digital photography.
## Three AI-Generated Leica Masterpieces ## Three AI-Generated Leica Masterpieces
### Image 1: Parisian Decisive Moment ### Image 1: Parisian Decisive Moment
![Paris Decisive Moment](/mock/api/threads/7f9dc56c-e49c-4671-a3d2-c492ff4dce0c/artifacts/mnt/user-data/outputs/leica-paris-decisive-moment.jpg) ![Paris Decisive Moment](/mock/api/threads/7f9dc56c-e49c-4671-a3d2-c492ff4dce0c/artifacts/mnt/user-data/outputs/leica-paris-decisive-moment.jpg)
This image captures the essence of Cartier-Bresson's philosophy. A woman in a red coat leaps over a puddle while a cyclist passes in perfect synchrony. The composition follows the rule of thirds, with the subject positioned at the intersection of grid lines. Shot with a simulated Leica M11 and 35mm Summicron lens at f/2.8, the image features shallow depth of field, natural film grain, and the warm, muted color palette characteristic of Leica photography. This image captures the essence of Cartier-Bresson's philosophy. A woman in a red coat leaps over a puddle while a cyclist passes in perfect synchrony. The composition follows the rule of thirds, with the subject positioned at the intersection of grid lines. Shot with a simulated Leica M11 and 35mm Summicron lens at f/2.8, the image features shallow depth of field, natural film grain, and the warm, muted color palette characteristic of Leica photography.
@ -34,6 +39,7 @@ This image captures the essence of Cartier-Bresson's philosophy. A woman in a re
The "decisive moment" here isn't just about timing—it's about the alignment of multiple elements: the woman's motion, the cyclist's position, the reflection in the puddle, and the directional morning light creating long shadows on wet cobblestones. The "decisive moment" here isn't just about timing—it's about the alignment of multiple elements: the woman's motion, the cyclist's position, the reflection in the puddle, and the directional morning light creating long shadows on wet cobblestones.
### Image 2: Tokyo Night Reflections ### Image 2: Tokyo Night Reflections
![Tokyo Night Scene](/mock/api/threads/7f9dc56c-e49c-4671-a3d2-c492ff4dce0c/artifacts/mnt/user-data/outputs/leica-tokyo-night.jpg) ![Tokyo Night Scene](/mock/api/threads/7f9dc56c-e49c-4671-a3d2-c492ff4dce0c/artifacts/mnt/user-data/outputs/leica-tokyo-night.jpg)
Moving to Shinjuku, Tokyo, this image explores the atmospheric possibilities of Leica's legendary Noctilux lens. Simulating a Leica M10-P with a 50mm f/0.95 Noctilux wide open, the image creates extremely shallow depth of field with beautiful bokeh balls from neon signs reflected in wet pavement. Moving to Shinjuku, Tokyo, this image explores the atmospheric possibilities of Leica's legendary Noctilux lens. Simulating a Leica M10-P with a 50mm f/0.95 Noctilux wide open, the image creates extremely shallow depth of field with beautiful bokeh balls from neon signs reflected in wet pavement.
@ -41,6 +47,7 @@ Moving to Shinjuku, Tokyo, this image explores the atmospheric possibilities of
A salaryman waits under glowing kanji signs, steam rising from a nearby ramen shop. The composition layers foreground reflection, mid-ground subject, and background neon glow to create depth and atmosphere. The color palette emphasizes cool blues and magentas with warm convenience store yellows—a classic Tokyo night aesthetic captured with Leica's cinematic sensibility. A salaryman waits under glowing kanji signs, steam rising from a nearby ramen shop. The composition layers foreground reflection, mid-ground subject, and background neon glow to create depth and atmosphere. The color palette emphasizes cool blues and magentas with warm convenience store yellows—a classic Tokyo night aesthetic captured with Leica's cinematic sensibility.
### Image 3: New York City Candid ### Image 3: New York City Candid
![NYC Candid Scene](/mock/api/threads/7f9dc56c-e49c-4671-a3d2-c492ff4dce0c/artifacts/mnt/user-data/outputs/leica-nyc-candid.jpg) ![NYC Candid Scene](/mock/api/threads/7f9dc56c-e49c-4671-a3d2-c492ff4dce0c/artifacts/mnt/user-data/outputs/leica-nyc-candid.jpg)
This Chinatown scene demonstrates the documentary power of Leica's Q2 camera with its fixed 28mm Summilux lens. The wide angle captures environmental context while maintaining intimate proximity to the subjects. A fishmonger hands a live fish to a customer while tourists photograph the scene—a moment of cultural contrast and authentic urban life. This Chinatown scene demonstrates the documentary power of Leica's Q2 camera with its fixed 28mm Summilux lens. The wide angle captures environmental context while maintaining intimate proximity to the subjects. A fishmonger hands a live fish to a customer while tourists photograph the scene—a moment of cultural contrast and authentic urban life.
@ -52,18 +59,23 @@ The 28mm perspective shows multiple layers: the transaction in foreground, touri
Creating these images required careful prompt engineering based on my research: Creating these images required careful prompt engineering based on my research:
### Camera and Lens Specifications ### Camera and Lens Specifications
Each prompt specified exact equipment: Each prompt specified exact equipment:
- **Paris**: Leica M11 with 35mm f/2 Summicron at f/2.8 - **Paris**: Leica M11 with 35mm f/2 Summicron at f/2.8
- **Tokyo**: Leica M10-P with 50mm f/0.95 Noctilux at f/0.95 - **Tokyo**: Leica M10-P with 50mm f/0.95 Noctilux at f/0.95
- **NYC**: Leica Q2 with fixed 28mm f/1.7 Summilux at f/2.8 - **NYC**: Leica Q2 with fixed 28mm f/1.7 Summilux at f/2.8
### Film Simulation ### Film Simulation
Different film stocks were simulated: Different film stocks were simulated:
- Kodak Portra 400 for Paris (natural skin tones, fine grain) - Kodak Portra 400 for Paris (natural skin tones, fine grain)
- Cinestill 800T for Tokyo (halation, cinematic look) - Cinestill 800T for Tokyo (halation, cinematic look)
- Kodak Ektar 100 for NYC (vibrant colors, fine grain) - Kodak Ektar 100 for NYC (vibrant colors, fine grain)
### Composition Principles ### Composition Principles
- Rule of thirds positioning - Rule of thirds positioning
- Environmental storytelling - Environmental storytelling
- Layers of depth (foreground, mid-ground, background) - Layers of depth (foreground, mid-ground, background)
@ -71,6 +83,7 @@ Different film stocks were simulated:
- Negative space for breathing room - Negative space for breathing room
### Lighting Characteristics ### Lighting Characteristics
- Natural, directional light sources - Natural, directional light sources
- Practical lighting (neon signs, shop windows) - Practical lighting (neon signs, shop windows)
- Atmospheric elements (rain, steam, smoke) - Atmospheric elements (rain, steam, smoke)
@ -81,12 +94,14 @@ Different film stocks were simulated:
These images demonstrate that AI can learn from photographic masters while creating original work. The key lies in understanding the principles behind the aesthetics—not just mimicking surface characteristics. These images demonstrate that AI can learn from photographic masters while creating original work. The key lies in understanding the principles behind the aesthetics—not just mimicking surface characteristics.
### What AI Gets Right: ### What AI Gets Right:
- Technical accuracy (bokeh, depth of field, grain) - Technical accuracy (bokeh, depth of field, grain)
- Composition principles - Composition principles
- Lighting simulation - Lighting simulation
- Environmental storytelling - Environmental storytelling
### What Remains Human: ### What Remains Human:
- Intentionality and concept development - Intentionality and concept development
- Emotional connection to subjects - Emotional connection to subjects
- Ethical considerations in street photography - Ethical considerations in street photography
@ -102,4 +117,4 @@ As AI continues to evolve, the most compelling work will likely come from those
--- ---
*All images generated using structured prompt engineering based on Leica photography research. Prompts available upon request.* _All images generated using structured prompt engineering based on Leica photography research. Prompts available upon request._

View File

@ -738,4 +738,4 @@
"interrupts": [], "interrupts": [],
"checkpoint_id": "1f0fbee1-86cb-630e-8035-fdef3b9e7862", "checkpoint_id": "1f0fbee1-86cb-630e-8035-fdef3b9e7862",
"parent_checkpoint_id": "1f0fbee1-86c7-6a6a-8034-0eba0e105137" "parent_checkpoint_id": "1f0fbee1-86c7-6a6a-8034-0eba0e105137"
} }

View File

@ -1,264 +1,354 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Pride and Prejudice | Jane Austen</title> <title>Pride and Prejudice | Jane Austen</title>
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400&family=Playfair+Display:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500&display=swap" rel="stylesheet"> <link
<link rel="stylesheet" href="styles.css"> href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400&family=Playfair+Display:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500&display=swap"
</head> rel="stylesheet"
<body> />
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<!-- Navigation --> <!-- Navigation -->
<nav class="nav"> <nav class="nav">
<div class="nav-brand">P&P</div> <div class="nav-brand">P&P</div>
<ul class="nav-links"> <ul class="nav-links">
<li><a href="#about">About</a></li> <li><a href="#about">About</a></li>
<li><a href="#characters">Characters</a></li> <li><a href="#characters">Characters</a></li>
<li><a href="#themes">Themes</a></li> <li><a href="#themes">Themes</a></li>
<li><a href="#quotes">Quotes</a></li> <li><a href="#quotes">Quotes</a></li>
</ul> </ul>
</nav> </nav>
<!-- Hero Section --> <!-- Hero Section -->
<section class="hero"> <section class="hero">
<div class="hero-bg"> <div class="hero-bg">
<div class="hero-pattern"></div> <div class="hero-pattern"></div>
</div> </div>
<div class="hero-content"> <div class="hero-content">
<p class="hero-subtitle">A Novel by</p> <p class="hero-subtitle">A Novel by</p>
<h1 class="hero-title"> <h1 class="hero-title">
<span class="title-line">Pride</span> <span class="title-line">Pride</span>
<span class="title-ampersand">&</span> <span class="title-ampersand">&</span>
<span class="title-line">Prejudice</span> <span class="title-line">Prejudice</span>
</h1> </h1>
<p class="hero-author">Jane Austen</p> <p class="hero-author">Jane Austen</p>
<p class="hero-year">1813</p> <p class="hero-year">1813</p>
<div class="hero-divider"> <div class="hero-divider">
<span class="divider-line"></span> <span class="divider-line"></span>
<span class="divider-ornament"></span> <span class="divider-ornament"></span>
<span class="divider-line"></span> <span class="divider-line"></span>
</div>
<p class="hero-tagline">"It is a truth universally acknowledged..."</p>
<a href="#about" class="hero-cta">
<span>Discover the Story</span>
<svg class="cta-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M12 5v14M5 12l7 7 7-7"/>
</svg>
</a>
</div>
<div class="hero-scroll-indicator">
<div class="scroll-line"></div>
</div> </div>
<p class="hero-tagline">"It is a truth universally acknowledged..."</p>
<a href="#about" class="hero-cta">
<span>Discover the Story</span>
<svg
class="cta-arrow"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
>
<path d="M12 5v14M5 12l7 7 7-7" />
</svg>
</a>
</div>
<div class="hero-scroll-indicator">
<div class="scroll-line"></div>
</div>
</section> </section>
<!-- About Section --> <!-- About Section -->
<section id="about" class="about"> <section id="about" class="about">
<div class="container"> <div class="container">
<div class="section-header"> <div class="section-header">
<span class="section-number">01</span> <span class="section-number">01</span>
<h2 class="section-title">The Novel</h2> <h2 class="section-title">The Novel</h2>
</div>
<div class="about-content">
<div class="about-text">
<p class="about-lead">Set in rural England in the early 19th century, <em>Pride and Prejudice</em> tells the story of the Bennet family and their five unmarried daughters.</p>
<p>When the wealthy and eligible Mr. Bingley rents a nearby estate, Mrs. Bennet sees an opportunity to marry off her eldest daughter, Jane. At a ball, Jane forms an attachment to Mr. Bingley, while her sister Elizabeth meets his friend, the proud Mr. Darcy.</p>
<p>What follows is a masterful exploration of manners, morality, education, and marriage in the society of the landed gentry of early 19th-century England.</p>
</div>
<div class="about-stats">
<div class="stat-item">
<span class="stat-number">61</span>
<span class="stat-label">Chapters</span>
</div>
<div class="stat-item">
<span class="stat-number">122K</span>
<span class="stat-label">Words</span>
</div>
<div class="stat-item">
<span class="stat-number">20M+</span>
<span class="stat-label">Copies Sold</span>
</div>
</div>
</div>
</div> </div>
<div class="about-content">
<div class="about-text">
<p class="about-lead">
Set in rural England in the early 19th century,
<em>Pride and Prejudice</em> tells the story of the Bennet family
and their five unmarried daughters.
</p>
<p>
When the wealthy and eligible Mr. Bingley rents a nearby estate,
Mrs. Bennet sees an opportunity to marry off her eldest daughter,
Jane. At a ball, Jane forms an attachment to Mr. Bingley, while
her sister Elizabeth meets his friend, the proud Mr. Darcy.
</p>
<p>
What follows is a masterful exploration of manners, morality,
education, and marriage in the society of the landed gentry of
early 19th-century England.
</p>
</div>
<div class="about-stats">
<div class="stat-item">
<span class="stat-number">61</span>
<span class="stat-label">Chapters</span>
</div>
<div class="stat-item">
<span class="stat-number">122K</span>
<span class="stat-label">Words</span>
</div>
<div class="stat-item">
<span class="stat-number">20M+</span>
<span class="stat-label">Copies Sold</span>
</div>
</div>
</div>
</div>
</section> </section>
<!-- Characters Section --> <!-- Characters Section -->
<section id="characters" class="characters"> <section id="characters" class="characters">
<div class="container"> <div class="container">
<div class="section-header"> <div class="section-header">
<span class="section-number">02</span> <span class="section-number">02</span>
<h2 class="section-title">The Characters</h2> <h2 class="section-title">The Characters</h2>
</div>
<div class="characters-grid">
<div class="character-card featured">
<div class="character-portrait elizabeth"></div>
<div class="character-info">
<h3>Elizabeth Bennet</h3>
<p class="character-role">The Protagonist</p>
<p class="character-desc">Intelligent, witty, and independent, Elizabeth navigates society's expectations while staying true to her principles.</p>
</div>
</div>
<div class="character-card featured">
<div class="character-portrait darcy"></div>
<div class="character-info">
<h3>Fitzwilliam Darcy</h3>
<p class="character-role">The Romantic Lead</p>
<p class="character-desc">Wealthy, reserved, and initially perceived as arrogant, Darcy's true character is revealed through his actions.</p>
</div>
</div>
<div class="character-card">
<div class="character-portrait jane"></div>
<div class="character-info">
<h3>Jane Bennet</h3>
<p class="character-role">The Eldest Sister</p>
<p class="character-desc">Beautiful, gentle, and always sees the best in people.</p>
</div>
</div>
<div class="character-card">
<div class="character-portrait bingley"></div>
<div class="character-info">
<h3>Charles Bingley</h3>
<p class="character-role">The Amiable Gentleman</p>
<p class="character-desc">Wealthy, good-natured, and easily influenced by his friends.</p>
</div>
</div>
<div class="character-card">
<div class="character-portrait lydia"></div>
<div class="character-info">
<h3>Lydia Bennet</h3>
<p class="character-role">The Youngest Sister</p>
<p class="character-desc">Frivolous, flirtatious, and impulsive, causing family scandal.</p>
</div>
</div>
<div class="character-card">
<div class="character-portrait wickham"></div>
<div class="character-info">
<h3>George Wickham</h3>
<p class="character-role">The Antagonist</p>
<p class="character-desc">Charming on the surface but deceitful and manipulative.</p>
</div>
</div>
</div>
</div> </div>
<div class="characters-grid">
<div class="character-card featured">
<div class="character-portrait elizabeth"></div>
<div class="character-info">
<h3>Elizabeth Bennet</h3>
<p class="character-role">The Protagonist</p>
<p class="character-desc">
Intelligent, witty, and independent, Elizabeth navigates
society's expectations while staying true to her principles.
</p>
</div>
</div>
<div class="character-card featured">
<div class="character-portrait darcy"></div>
<div class="character-info">
<h3>Fitzwilliam Darcy</h3>
<p class="character-role">The Romantic Lead</p>
<p class="character-desc">
Wealthy, reserved, and initially perceived as arrogant, Darcy's
true character is revealed through his actions.
</p>
</div>
</div>
<div class="character-card">
<div class="character-portrait jane"></div>
<div class="character-info">
<h3>Jane Bennet</h3>
<p class="character-role">The Eldest Sister</p>
<p class="character-desc">
Beautiful, gentle, and always sees the best in people.
</p>
</div>
</div>
<div class="character-card">
<div class="character-portrait bingley"></div>
<div class="character-info">
<h3>Charles Bingley</h3>
<p class="character-role">The Amiable Gentleman</p>
<p class="character-desc">
Wealthy, good-natured, and easily influenced by his friends.
</p>
</div>
</div>
<div class="character-card">
<div class="character-portrait lydia"></div>
<div class="character-info">
<h3>Lydia Bennet</h3>
<p class="character-role">The Youngest Sister</p>
<p class="character-desc">
Frivolous, flirtatious, and impulsive, causing family scandal.
</p>
</div>
</div>
<div class="character-card">
<div class="character-portrait wickham"></div>
<div class="character-info">
<h3>George Wickham</h3>
<p class="character-role">The Antagonist</p>
<p class="character-desc">
Charming on the surface but deceitful and manipulative.
</p>
</div>
</div>
</div>
</div>
</section> </section>
<!-- Themes Section --> <!-- Themes Section -->
<section id="themes" class="themes"> <section id="themes" class="themes">
<div class="container"> <div class="container">
<div class="section-header"> <div class="section-header">
<span class="section-number">03</span> <span class="section-number">03</span>
<h2 class="section-title">Themes</h2> <h2 class="section-title">Themes</h2>
</div>
<div class="themes-content">
<div class="theme-item">
<div class="theme-icon">
<svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M24 44c11.046 0 20-8.954 20-20S35.046 4 24 4 4 12.954 4 24s8.954 20 20 20z"/>
<path d="M24 12v20M16 24h16"/>
</svg>
</div>
<h3>Pride</h3>
<p>Darcy's pride in his social position initially prevents him from acknowledging his feelings for Elizabeth, while Elizabeth's pride in her discernment blinds her to Darcy's true character.</p>
</div>
<div class="theme-item">
<div class="theme-icon">
<svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M24 4l6 12 13 2-9 9 2 13-12-6-12 6 2-13-9-9 13-2z"/>
</svg>
</div>
<h3>Prejudice</h3>
<p>Elizabeth's prejudice against Darcy, formed from their first meeting and Wickham's lies, nearly costs her happiness. The novel shows how first impressions can be misleading.</p>
</div>
<div class="theme-item">
<div class="theme-icon">
<svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M24 44c11.046 0 20-8.954 20-20S35.046 4 24 4 4 12.954 4 24s8.954 20 20 20z"/>
<path d="M14 24c0-5.523 4.477-10 10-10s10 4.477 10 10-4.477 10-10 10"/>
</svg>
</div>
<h3>Marriage</h3>
<p>The novel examines marriage from multiple perspectives: for love, for security, for social advancement, and the rare ideal of marrying for both love and compatibility.</p>
</div>
<div class="theme-item">
<div class="theme-icon">
<svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M12 12h24v24H12z"/>
<path d="M12 12l24 24M36 12L12 36"/>
</svg>
</div>
<h3>Class</h3>
<p>The rigid class structure of Regency England shapes every interaction, from who may marry whom to how characters are judged by their connections and fortune.</p>
</div>
</div>
</div> </div>
<div class="themes-content">
<div class="theme-item">
<div class="theme-icon">
<svg
viewBox="0 0 48 48"
fill="none"
stroke="currentColor"
stroke-width="1.5"
>
<path
d="M24 44c11.046 0 20-8.954 20-20S35.046 4 24 4 4 12.954 4 24s8.954 20 20 20z"
/>
<path d="M24 12v20M16 24h16" />
</svg>
</div>
<h3>Pride</h3>
<p>
Darcy's pride in his social position initially prevents him from
acknowledging his feelings for Elizabeth, while Elizabeth's pride
in her discernment blinds her to Darcy's true character.
</p>
</div>
<div class="theme-item">
<div class="theme-icon">
<svg
viewBox="0 0 48 48"
fill="none"
stroke="currentColor"
stroke-width="1.5"
>
<path d="M24 4l6 12 13 2-9 9 2 13-12-6-12 6 2-13-9-9 13-2z" />
</svg>
</div>
<h3>Prejudice</h3>
<p>
Elizabeth's prejudice against Darcy, formed from their first
meeting and Wickham's lies, nearly costs her happiness. The novel
shows how first impressions can be misleading.
</p>
</div>
<div class="theme-item">
<div class="theme-icon">
<svg
viewBox="0 0 48 48"
fill="none"
stroke="currentColor"
stroke-width="1.5"
>
<path
d="M24 44c11.046 0 20-8.954 20-20S35.046 4 24 4 4 12.954 4 24s8.954 20 20 20z"
/>
<path
d="M14 24c0-5.523 4.477-10 10-10s10 4.477 10 10-4.477 10-10 10"
/>
</svg>
</div>
<h3>Marriage</h3>
<p>
The novel examines marriage from multiple perspectives: for love,
for security, for social advancement, and the rare ideal of
marrying for both love and compatibility.
</p>
</div>
<div class="theme-item">
<div class="theme-icon">
<svg
viewBox="0 0 48 48"
fill="none"
stroke="currentColor"
stroke-width="1.5"
>
<path d="M12 12h24v24H12z" />
<path d="M12 12l24 24M36 12L12 36" />
</svg>
</div>
<h3>Class</h3>
<p>
The rigid class structure of Regency England shapes every
interaction, from who may marry whom to how characters are judged
by their connections and fortune.
</p>
</div>
</div>
</div>
</section> </section>
<!-- Famous Quotes Section --> <!-- Famous Quotes Section -->
<section id="quotes" class="quotes"> <section id="quotes" class="quotes">
<div class="container"> <div class="container">
<div class="section-header"> <div class="section-header">
<span class="section-number">04</span> <span class="section-number">04</span>
<h2 class="section-title">Memorable Quotes</h2> <h2 class="section-title">Memorable Quotes</h2>
</div>
<div class="quotes-slider">
<div class="quote-card active">
<span class="quote-mark">"</span>
<blockquote>It is a truth universally acknowledged, that a single man in possession of a good fortune, must be in want of a wife.</blockquote>
<cite>— Opening Line</cite>
</div>
<div class="quote-card">
<span class="quote-mark">"</span>
<blockquote>I could easily forgive his pride, if he had not mortified mine.</blockquote>
<cite>— Elizabeth Bennet</cite>
</div>
<div class="quote-card">
<span class="quote-mark">"</span>
<blockquote>You have bewitched me, body and soul, and I love, I love, I love you.</blockquote>
<cite>— Mr. Darcy</cite>
</div>
<div class="quote-card">
<span class="quote-mark">"</span>
<blockquote>Till this moment I never knew myself.</blockquote>
<cite>— Elizabeth Bennet</cite>
</div>
<div class="quote-card">
<span class="quote-mark">"</span>
<blockquote>My good opinion once lost, is lost forever.</blockquote>
<cite>— Mr. Darcy</cite>
</div>
</div>
<div class="quotes-nav">
<button class="quote-dot active" data-index="0"></button>
<button class="quote-dot" data-index="1"></button>
<button class="quote-dot" data-index="2"></button>
<button class="quote-dot" data-index="3"></button>
<button class="quote-dot" data-index="4"></button>
</div>
</div> </div>
<div class="quotes-slider">
<div class="quote-card active">
<span class="quote-mark">"</span>
<blockquote>
It is a truth universally acknowledged, that a single man in
possession of a good fortune, must be in want of a wife.
</blockquote>
<cite>— Opening Line</cite>
</div>
<div class="quote-card">
<span class="quote-mark">"</span>
<blockquote>
I could easily forgive his pride, if he had not mortified mine.
</blockquote>
<cite>— Elizabeth Bennet</cite>
</div>
<div class="quote-card">
<span class="quote-mark">"</span>
<blockquote>
You have bewitched me, body and soul, and I love, I love, I love
you.
</blockquote>
<cite>— Mr. Darcy</cite>
</div>
<div class="quote-card">
<span class="quote-mark">"</span>
<blockquote>Till this moment I never knew myself.</blockquote>
<cite>— Elizabeth Bennet</cite>
</div>
<div class="quote-card">
<span class="quote-mark">"</span>
<blockquote>My good opinion once lost, is lost forever.</blockquote>
<cite>— Mr. Darcy</cite>
</div>
</div>
<div class="quotes-nav">
<button class="quote-dot active" data-index="0"></button>
<button class="quote-dot" data-index="1"></button>
<button class="quote-dot" data-index="2"></button>
<button class="quote-dot" data-index="3"></button>
<button class="quote-dot" data-index="4"></button>
</div>
</div>
</section> </section>
<!-- Footer --> <!-- Footer -->
<footer class="footer"> <footer class="footer">
<div class="container"> <div class="container">
<div class="footer-content"> <div class="footer-content">
<div class="footer-brand"> <div class="footer-brand">
<span class="footer-logo">P&P</span> <span class="footer-logo">P&P</span>
<p>A timeless masterpiece of English literature</p> <p>A timeless masterpiece of English literature</p>
</div> </div>
<div class="footer-divider"> <div class="footer-divider">
<span class="divider-ornament"></span> <span class="divider-ornament"></span>
</div> </div>
<p class="footer-credit">Based on the 1813 novel by Jane Austen</p> <p class="footer-credit">Based on the 1813 novel by Jane Austen</p>
</div>
</div> </div>
<a href="https://deerflow.tech" target="_blank" class="deerflow-signature"> </div>
<span class="signature-text">Created By Deerflow</span> <a
<span class="signature-icon"></span> href="https://deerflow.tech"
</a> target="_blank"
class="deerflow-signature"
>
<span class="signature-text">Created By Deerflow</span>
<span class="signature-icon"></span>
</a>
</footer> </footer>
<script src="script.js"></script> <script src="script.js"></script>
</body> </body>
</html> </html>

View File

@ -1,177 +1,180 @@
// Pride and Prejudice - Interactive Features // Pride and Prejudice - Interactive Features
document.addEventListener('DOMContentLoaded', () => { document.addEventListener("DOMContentLoaded", () => {
// Navigation scroll effect // Navigation scroll effect
initNavigation(); initNavigation();
// Quotes slider // Quotes slider
initQuotesSlider(); initQuotesSlider();
// Scroll reveal animations // Scroll reveal animations
initScrollReveal(); initScrollReveal();
// Smooth scroll for anchor links // Smooth scroll for anchor links
initSmoothScroll(); initSmoothScroll();
}); });
// ============================================ // ============================================
// NAVIGATION SCROLL EFFECT // NAVIGATION SCROLL EFFECT
// ============================================ // ============================================
function initNavigation() { function initNavigation() {
const nav = document.querySelector('.nav'); const nav = document.querySelector(".nav");
let lastScroll = 0; let lastScroll = 0;
window.addEventListener('scroll', () => { window.addEventListener("scroll", () => {
const currentScroll = window.pageYOffset; const currentScroll = window.pageYOffset;
// Add/remove scrolled class // Add/remove scrolled class
if (currentScroll > 100) { if (currentScroll > 100) {
nav.classList.add('scrolled'); nav.classList.add("scrolled");
} else { } else {
nav.classList.remove('scrolled'); nav.classList.remove("scrolled");
} }
lastScroll = currentScroll; lastScroll = currentScroll;
}); });
} }
// ============================================ // ============================================
// QUOTES SLIDER // QUOTES SLIDER
// ============================================ // ============================================
function initQuotesSlider() { function initQuotesSlider() {
const quotes = document.querySelectorAll('.quote-card'); const quotes = document.querySelectorAll(".quote-card");
const dots = document.querySelectorAll('.quote-dot'); const dots = document.querySelectorAll(".quote-dot");
let currentIndex = 0; let currentIndex = 0;
let autoSlideInterval; let autoSlideInterval;
function showQuote(index) { function showQuote(index) {
// Remove active class from all quotes and dots // Remove active class from all quotes and dots
quotes.forEach(quote => quote.classList.remove('active')); quotes.forEach((quote) => quote.classList.remove("active"));
dots.forEach(dot => dot.classList.remove('active')); dots.forEach((dot) => dot.classList.remove("active"));
// Add active class to current quote and dot // Add active class to current quote and dot
quotes[index].classList.add('active'); quotes[index].classList.add("active");
dots[index].classList.add('active'); dots[index].classList.add("active");
currentIndex = index; currentIndex = index;
} }
function nextQuote() { function nextQuote() {
const nextIndex = (currentIndex + 1) % quotes.length; const nextIndex = (currentIndex + 1) % quotes.length;
showQuote(nextIndex); showQuote(nextIndex);
} }
// Dot click handlers // Dot click handlers
dots.forEach((dot, index) => { dots.forEach((dot, index) => {
dot.addEventListener('click', () => { dot.addEventListener("click", () => {
showQuote(index); showQuote(index);
resetAutoSlide(); resetAutoSlide();
});
}); });
});
// Auto-slide functionality
function startAutoSlide() { // Auto-slide functionality
autoSlideInterval = setInterval(nextQuote, 6000); function startAutoSlide() {
} autoSlideInterval = setInterval(nextQuote, 6000);
}
function resetAutoSlide() {
clearInterval(autoSlideInterval); function resetAutoSlide() {
startAutoSlide(); clearInterval(autoSlideInterval);
}
// Start auto-slide
startAutoSlide(); startAutoSlide();
}
// Pause on hover
const slider = document.querySelector('.quotes-slider'); // Start auto-slide
slider.addEventListener('mouseenter', () => clearInterval(autoSlideInterval)); startAutoSlide();
slider.addEventListener('mouseleave', startAutoSlide);
// Pause on hover
const slider = document.querySelector(".quotes-slider");
slider.addEventListener("mouseenter", () => clearInterval(autoSlideInterval));
slider.addEventListener("mouseleave", startAutoSlide);
} }
// ============================================ // ============================================
// SCROLL REVEAL ANIMATIONS // SCROLL REVEAL ANIMATIONS
// ============================================ // ============================================
function initScrollReveal() { function initScrollReveal() {
const revealElements = document.querySelectorAll( const revealElements = document.querySelectorAll(
'.about-content, .character-card, .theme-item, .section-header' ".about-content, .character-card, .theme-item, .section-header",
); );
const revealOptions = { const revealOptions = {
threshold: 0.15, threshold: 0.15,
rootMargin: '0px 0px -50px 0px' rootMargin: "0px 0px -50px 0px",
}; };
const revealObserver = new IntersectionObserver((entries) => { const revealObserver = new IntersectionObserver((entries) => {
entries.forEach((entry, index) => { entries.forEach((entry, index) => {
if (entry.isIntersecting) { if (entry.isIntersecting) {
// Add staggered delay for grid items // Add staggered delay for grid items
const delay = entry.target.classList.contains('character-card') || const delay =
entry.target.classList.contains('theme-item') entry.target.classList.contains("character-card") ||
? index * 100 entry.target.classList.contains("theme-item")
: 0; ? index * 100
: 0;
setTimeout(() => {
entry.target.classList.add('reveal'); setTimeout(() => {
entry.target.style.opacity = '1'; entry.target.classList.add("reveal");
entry.target.style.transform = 'translateY(0)'; entry.target.style.opacity = "1";
}, delay); entry.target.style.transform = "translateY(0)";
}, delay);
revealObserver.unobserve(entry.target);
} revealObserver.unobserve(entry.target);
}); }
}, revealOptions);
revealElements.forEach(el => {
el.style.opacity = '0';
el.style.transform = 'translateY(30px)';
el.style.transition = 'opacity 0.8s cubic-bezier(0.16, 1, 0.3, 1), transform 0.8s cubic-bezier(0.16, 1, 0.3, 1)';
revealObserver.observe(el);
}); });
}, revealOptions);
revealElements.forEach((el) => {
el.style.opacity = "0";
el.style.transform = "translateY(30px)";
el.style.transition =
"opacity 0.8s cubic-bezier(0.16, 1, 0.3, 1), transform 0.8s cubic-bezier(0.16, 1, 0.3, 1)";
revealObserver.observe(el);
});
} }
// ============================================ // ============================================
// SMOOTH SCROLL FOR ANCHOR LINKS // SMOOTH SCROLL FOR ANCHOR LINKS
// ============================================ // ============================================
function initSmoothScroll() { function initSmoothScroll() {
document.querySelectorAll('a[href^="#"]').forEach(anchor => { document.querySelectorAll('a[href^="#"]').forEach((anchor) => {
anchor.addEventListener('click', function(e) { anchor.addEventListener("click", function (e) {
e.preventDefault(); e.preventDefault();
const target = document.querySelector(this.getAttribute('href')); const target = document.querySelector(this.getAttribute("href"));
if (target) { if (target) {
const navHeight = document.querySelector('.nav').offsetHeight; const navHeight = document.querySelector(".nav").offsetHeight;
const targetPosition = target.getBoundingClientRect().top + window.pageYOffset - navHeight; const targetPosition =
target.getBoundingClientRect().top + window.pageYOffset - navHeight;
window.scrollTo({
top: targetPosition, window.scrollTo({
behavior: 'smooth' top: targetPosition,
}); behavior: "smooth",
}
}); });
}
}); });
});
} }
// ============================================ // ============================================
// PARALLAX EFFECT FOR HERO // PARALLAX EFFECT FOR HERO
// ============================================ // ============================================
window.addEventListener('scroll', () => { window.addEventListener("scroll", () => {
const scrolled = window.pageYOffset; const scrolled = window.pageYOffset;
const heroPattern = document.querySelector('.hero-pattern'); const heroPattern = document.querySelector(".hero-pattern");
if (heroPattern && scrolled < window.innerHeight) { if (heroPattern && scrolled < window.innerHeight) {
heroPattern.style.transform = `translateY(${scrolled * 0.3}px) rotate(${scrolled * 0.02}deg)`; heroPattern.style.transform = `translateY(${scrolled * 0.3}px) rotate(${scrolled * 0.02}deg)`;
} }
}); });
// ============================================ // ============================================
// CHARACTER CARD HOVER EFFECT // CHARACTER CARD HOVER EFFECT
// ============================================ // ============================================
document.querySelectorAll('.character-card').forEach(card => { document.querySelectorAll(".character-card").forEach((card) => {
card.addEventListener('mouseenter', function() { card.addEventListener("mouseenter", function () {
this.style.zIndex = '10'; this.style.zIndex = "10";
}); });
card.addEventListener('mouseleave', function() { card.addEventListener("mouseleave", function () {
this.style.zIndex = '1'; this.style.zIndex = "1";
}); });
}); });

View File

@ -1187,9 +1187,7 @@
{ {
"name": "present_files", "name": "present_files",
"args": { "args": {
"filepaths": [ "filepaths": ["/mnt/user-data/outputs/diana_hu_research.md"]
"/mnt/user-data/outputs/diana_hu_research.md"
]
}, },
"id": "call_fiwu3557asbu155mzi3ore72", "id": "call_fiwu3557asbu155mzi3ore72",
"type": "tool_call" "type": "tool_call"
@ -1235,9 +1233,7 @@
}, },
"uploaded_files": [], "uploaded_files": [],
"title": "Startup Tech Lead's Video Analysis Request", "title": "Startup Tech Lead's Video Analysis Request",
"artifacts": [ "artifacts": ["/mnt/user-data/outputs/diana_hu_research.md"],
"/mnt/user-data/outputs/diana_hu_research.md"
],
"todos": [ "todos": [
{ {
"content": "Analyze video transcript and extract key insights, advice, and frameworks", "content": "Analyze video transcript and extract key insights, advice, and frameworks",
@ -1307,4 +1303,4 @@
"interrupts": [], "interrupts": [],
"checkpoint_id": "1f0f99e8-0d7c-62d2-8051-7cb8958145fe", "checkpoint_id": "1f0f99e8-0d7c-62d2-8051-7cb8958145fe",
"parent_checkpoint_id": "1f0f99e8-0437-6ac8-8050-ce8edd831bf1" "parent_checkpoint_id": "1f0f99e8-0437-6ac8-8050-ce8edd831bf1"
} }

View File

@ -1,6 +1,7 @@
# Diana Hu: Technical Startup Founder Advice - Comprehensive Research # Diana Hu: Technical Startup Founder Advice - Comprehensive Research
## Video Overview ## Video Overview
**Title:** Tips For Technical Startup Founders | Startup School **Title:** Tips For Technical Startup Founders | Startup School
**Speaker:** Diana Hu, Y Combinator Group Partner **Speaker:** Diana Hu, Y Combinator Group Partner
**Date:** April 21, 2023 **Date:** April 21, 2023
@ -10,11 +11,13 @@
## Speaker Background ## Speaker Background
### Education ### Education
- **BS and MS in Electrical and Computer Engineering** from Carnegie Mellon University - **BS and MS in Electrical and Computer Engineering** from Carnegie Mellon University
- Focus on **computer vision and machine learning** - Focus on **computer vision and machine learning**
- Originally from Chile - Originally from Chile
### Career Path ### Career Path
1. **Co-founder & CTO of Escher Reality** (YC S17) 1. **Co-founder & CTO of Escher Reality** (YC S17)
- Startup building augmented reality SDK for game developers - Startup building augmented reality SDK for game developers
- Company acquired by Niantic (makers of Pokémon Go) in February 2018 - Company acquired by Niantic (makers of Pokémon Go) in February 2018
@ -29,11 +32,13 @@
- Specializes in technical founder guidance - Specializes in technical founder guidance
### Key Achievements ### Key Achievements
- Successfully built and sold AR startup to Niantic - Successfully built and sold AR startup to Niantic
- Scaled systems from prototype to millions of users - Scaled systems from prototype to millions of users
- Extensive experience mentoring technical founders - Extensive experience mentoring technical founders
## Escher Reality Acquisition ## Escher Reality Acquisition
- **Founded:** 2016 - **Founded:** 2016
- **Y Combinator Batch:** Summer 2017 (S17) - **Y Combinator Batch:** Summer 2017 (S17)
- **Product:** Augmented Reality backend/SDK for cross-platform mobile AR - **Product:** Augmented Reality backend/SDK for cross-platform mobile AR
@ -47,14 +52,17 @@
### Three Stages of Technical Founder Journey ### Three Stages of Technical Founder Journey
#### Stage 1: Ideating (0:00-8:30) #### Stage 1: Ideating (0:00-8:30)
**Goal:** Build a prototype as soon as possible (matter of days) **Goal:** Build a prototype as soon as possible (matter of days)
**Key Principles:** **Key Principles:**
- Build something to show/demo to users - Build something to show/demo to users
- Doesn't have to work fully - Doesn't have to work fully
- CEO co-founder should be finding users to show prototype - CEO co-founder should be finding users to show prototype
**Examples:** **Examples:**
1. **Optimizely** (YC W10) 1. **Optimizely** (YC W10)
- Built prototype in couple of days - Built prototype in couple of days
- JavaScript file on S3 for A/B testing - JavaScript file on S3 for A/B testing
@ -71,11 +79,13 @@
- Enough to get users excited despite hard tech - Enough to get users excited despite hard tech
**Common Mistakes:** **Common Mistakes:**
- Overbuilding at this stage - Overbuilding at this stage
- Not talking/listening to users soon enough - Not talking/listening to users soon enough
- Getting too attached to initial ideas - Getting too attached to initial ideas
#### Stage 2: Building MVP (8:30-19:43) #### Stage 2: Building MVP (8:30-19:43)
**Goal:** Build to launch quickly (weeks, not months) **Goal:** Build to launch quickly (weeks, not months)
**Key Principles:** **Key Principles:**
@ -96,6 +106,7 @@
- Don't build from scratch - Don't build from scratch
**Examples:** **Examples:**
1. **DoorDash** (originally Palo Alto Delivery) 1. **DoorDash** (originally Palo Alto Delivery)
- Static HTML with PDF menus - Static HTML with PDF menus
- Google Forms for orders - Google Forms for orders
@ -114,12 +125,14 @@
- Hired "misfits" overlooked by Google - Hired "misfits" overlooked by Google
**Tech Stack Philosophy:** **Tech Stack Philosophy:**
- "If you build a company and it works, tech choices don't matter as much" - "If you build a company and it works, tech choices don't matter as much"
- Facebook: PHP → HipHop transpiler - Facebook: PHP → HipHop transpiler
- JavaScript: V8 engine optimization - JavaScript: V8 engine optimization
- Choose what you're dangerous enough with - Choose what you're dangerous enough with
#### Stage 3: Launch Stage (19:43-26:51) #### Stage 3: Launch Stage (19:43-26:51)
**Goal:** Iterate towards product-market fit **Goal:** Iterate towards product-market fit
**Key Principles:** **Key Principles:**
@ -140,6 +153,7 @@
- Fix only what prevents product-market fit - Fix only what prevents product-market fit
**Examples:** **Examples:**
1. **WePay** (YC company) 1. **WePay** (YC company)
- Started as B2C payments (Venmo-like) - Started as B2C payments (Venmo-like)
- Analytics showed features unused - Analytics showed features unused
@ -159,6 +173,7 @@
- Added Node, PHP, WordPress support based on feedback - Added Node, PHP, WordPress support based on feedback
### Role Evolution Post Product-Market Fit ### Role Evolution Post Product-Market Fit
- **2-5 engineers:** 70% coding time - **2-5 engineers:** 70% coding time
- **5-10 engineers:** <50% coding time - **5-10 engineers:** <50% coding time
- **Beyond 10 engineers:** Little to no coding time - **Beyond 10 engineers:** Little to no coding time
@ -167,17 +182,20 @@
## Key Concepts Deep Dive ## Key Concepts Deep Dive
### 90/10 Solution (Paul Buchheit) ### 90/10 Solution (Paul Buchheit)
- Find ways to get 90% of the value with 10% of the effort - Find ways to get 90% of the value with 10% of the effort
- Available 90% solution now is better than 100% solution later - Available 90% solution now is better than 100% solution later
- Restrict product dimensions: geography, user type, data type, functionality - Restrict product dimensions: geography, user type, data type, functionality
### Technical Debt in Startups ### Technical Debt in Startups
- **Early stage:** Embrace technical debt - **Early stage:** Embrace technical debt
- **Post product-market fit:** Address scaling issues - **Post product-market fit:** Address scaling issues
- **Philosophy:** "Tech debt is totally fine - feel the heat of your tech burning" - **Philosophy:** "Tech debt is totally fine - feel the heat of your tech burning"
- Only fix what prevents reaching product-market fit - Only fix what prevents reaching product-market fit
### MVP Principles ### MVP Principles
1. **Speed over perfection:** Launch in weeks, not months 1. **Speed over perfection:** Launch in weeks, not months
2. **Manual processes:** Founders do unscalable work 2. **Manual processes:** Founders do unscalable work
3. **Limited scope:** Constrain to prove core value 3. **Limited scope:** Constrain to prove core value
@ -186,30 +204,35 @@
## Companies Mentioned (with Context) ## Companies Mentioned (with Context)
### Optimizely (YC W10) ### Optimizely (YC W10)
- A/B testing platform - A/B testing platform
- Prototype: JavaScript file on S3, manual execution - Prototype: JavaScript file on S3, manual execution
- Founders: Pete Koomen and Dan Siroker - Founders: Pete Koomen and Dan Siroker
- Dan previously headed analytics for Obama campaign - Dan previously headed analytics for Obama campaign
### Remora (YC W21) ### Remora (YC W21)
- Carbon capture device for semi-trucks - Carbon capture device for semi-trucks
- Prototype: 3D renderings to demonstrate concept - Prototype: 3D renderings to demonstrate concept
- Captures 80%+ of truck emissions - Captures 80%+ of truck emissions
- Can make trucks carbon-negative with biofuels - Can make trucks carbon-negative with biofuels
### Justin TV/Twitch ### Justin TV/Twitch
- Live streaming platform → gaming focus - Live streaming platform → gaming focus
- Founders: Justin Kan, Emmett Shear, Michael Seibel, Kyle Vogt - Founders: Justin Kan, Emmett Shear, Michael Seibel, Kyle Vogt
- MVP built by 4 founders (3 technical) - MVP built by 4 founders (3 technical)
- Hired overlooked engineers from Google - Hired overlooked engineers from Google
### Stripe ### Stripe
- Payment processing API - Payment processing API
- Early days: Founders manually processed payments - Early days: Founders manually processed payments
- Filled bank forms manually for each transaction - Filled bank forms manually for each transaction
- Classic "do things that don't scale" example - Classic "do things that don't scale" example
### DoorDash ### DoorDash
- Originally "Palo Alto Delivery" - Originally "Palo Alto Delivery"
- Static HTML with PDF menus - Static HTML with PDF menus
- Google Forms for orders - Google Forms for orders
@ -217,18 +240,21 @@
- Focused on suburbs vs metro areas (competitive advantage) - Focused on suburbs vs metro areas (competitive advantage)
### WayUp (YC 2015) ### WayUp (YC 2015)
- Job board for college students - Job board for college students
- CTO JJ chose Django/Python over Ruby/Rails - CTO JJ chose Django/Python over Ruby/Rails
- Prioritized iteration speed over popular choice - Prioritized iteration speed over popular choice
- Simple, effective tech stack - Simple, effective tech stack
### WePay (YC company) ### WePay (YC company)
- Started as B2C payments (Venmo competitor) - Started as B2C payments (Venmo competitor)
- Pivoted to API after user discovery - Pivoted to API after user discovery
- GoFundMe became key customer - GoFundMe became key customer
- Example of data + user interviews driving pivot - Example of data + user interviews driving pivot
### Segment ### Segment
- Analytics infrastructure - Analytics infrastructure
- Multiple launches in short timeframe - Multiple launches in short timeframe
- Started with limited integrations - Started with limited integrations
@ -236,36 +262,42 @@
- Acquired by Twilio for $3.2B - Acquired by Twilio for $3.2B
### Algolia ### Algolia
- Search API mentioned as YC success - Search API mentioned as YC success
- Part of Diana's network of advised companies - Part of Diana's network of advised companies
## Actionable Advice for Technical Founders ## Actionable Advice for Technical Founders
### Immediate Actions (Week 1) ### Immediate Actions (Week 1)
1. **Build clickable prototype** (Figma, InVision) in 1-3 days 1. **Build clickable prototype** (Figma, InVision) in 1-3 days
2. **Find 10 potential users** to show prototype 2. **Find 10 potential users** to show prototype
3. **Use existing tools** rather than building from scratch 3. **Use existing tools** rather than building from scratch
4. **Embrace ugly code** - it's temporary 4. **Embrace ugly code** - it's temporary
### Tech Stack Selection ### Tech Stack Selection
1. **Choose familiarity over trendiness** 1. **Choose familiarity over trendiness**
2. **Use third-party services** for non-core functions 2. **Use third-party services** for non-core functions
3. **Keep infrastructure simple** (Heroku, Firebase, AWS) 3. **Keep infrastructure simple** (Heroku, Firebase, AWS)
4. **Only build what's unique** to your value proposition 4. **Only build what's unique** to your value proposition
### Hiring Strategy ### Hiring Strategy
1. **Don't hire too early** (slows you down) 1. **Don't hire too early** (slows you down)
2. **Founders must build** to gain product insights 2. **Founders must build** to gain product insights
3. **Look for "misfits"** - overlooked talent 3. **Look for "misfits"** - overlooked talent
4. **Post product-market fit:** Scale team strategically 4. **Post product-market fit:** Scale team strategically
### Launch Strategy ### Launch Strategy
1. **Launch multiple times** (weekly iterations) 1. **Launch multiple times** (weekly iterations)
2. **Combine analytics with user interviews** 2. **Combine analytics with user interviews**
3. **Balance feature development with bug fixes** 3. **Balance feature development with bug fixes**
4. **Accept technical debt** until product-market fit 4. **Accept technical debt** until product-market fit
### Mindset Shifts ### Mindset Shifts
1. **From perfectionist to pragmatist** 1. **From perfectionist to pragmatist**
2. **From specialist to generalist** (do whatever it takes) 2. **From specialist to generalist** (do whatever it takes)
3. **From employee to owner** (no task beneath you) 3. **From employee to owner** (no task beneath you)
@ -274,12 +306,14 @@
## Diana's Personal Insights ## Diana's Personal Insights
### From Her Experience ### From Her Experience
- "Technical founder is committed to the success of your company" - "Technical founder is committed to the success of your company"
- "Do whatever it takes to get it to work" - "Do whatever it takes to get it to work"
- "Your product will evolve - if someone else builds it, you miss key learnings" - "Your product will evolve - if someone else builds it, you miss key learnings"
- "The only tech choices that matter are tied to customer promises" - "The only tech choices that matter are tied to customer promises"
### Common Traps to Avoid ### Common Traps to Avoid
1. **"What would Google do?"** - Building like a big company too early 1. **"What would Google do?"** - Building like a big company too early
2. **Hiring to move faster** - Actually slows you down initially 2. **Hiring to move faster** - Actually slows you down initially
3. **Over-fixing vs building** - Focus on product-market fit first 3. **Over-fixing vs building** - Focus on product-market fit first
@ -288,12 +322,14 @@
## Resources & References ## Resources & References
### YC Resources ### YC Resources
- Y Combinator Library: "Tips for technical startup founders" - Y Combinator Library: "Tips for technical startup founders"
- Paul Graham Essay: "Do Things That Don't Scale" - Paul Graham Essay: "Do Things That Don't Scale"
- Paul Buchheit Concept: "90/10 Solution" - Paul Buchheit Concept: "90/10 Solution"
- Startup School: Technical founder track - Startup School: Technical founder track
### Tools Mentioned ### Tools Mentioned
- **Prototyping:** Figma, InVision - **Prototyping:** Figma, InVision
- **Analytics:** Google Analytics, Amplitude, Mixpanel - **Analytics:** Google Analytics, Amplitude, Mixpanel
- **Infrastructure:** Heroku, Firebase, AWS, GCP - **Infrastructure:** Heroku, Firebase, AWS, GCP
@ -302,6 +338,7 @@
- **Landing Pages:** Webflow - **Landing Pages:** Webflow
### Further Reading ### Further Reading
1. Paul Graham essays (paulgraham.com) 1. Paul Graham essays (paulgraham.com)
2. Y Combinator Startup School materials 2. Y Combinator Startup School materials
3. Case studies: Stripe, DoorDash, Segment early days 3. Case studies: Stripe, DoorDash, Segment early days
@ -310,18 +347,21 @@
## Key Takeaways ## Key Takeaways
### For Technical Founders ### For Technical Founders
1. **Speed is your superpower** - Move faster than established companies 1. **Speed is your superpower** - Move faster than established companies
2. **Embrace imperfection** - Good enough beats perfect when speed matters 2. **Embrace imperfection** - Good enough beats perfect when speed matters
3. **Stay close to users** - Insights come from conversations, not just data 3. **Stay close to users** - Insights come from conversations, not just data
4. **Tech debt is a feature, not a bug** - Early stage startups should accumulate it 4. **Tech debt is a feature, not a bug** - Early stage startups should accumulate it
### For Startup Strategy ### For Startup Strategy
1. **Constrained focus** leads to better unit economics (DoorDash example) 1. **Constrained focus** leads to better unit economics (DoorDash example)
2. **Manual processes** create customer intimacy and learning 2. **Manual processes** create customer intimacy and learning
3. **Continuous launching** builds momentum and feedback loops 3. **Continuous launching** builds momentum and feedback loops
4. **Break things at scale** is a good problem to have 4. **Break things at scale** is a good problem to have
### For Team Building ### For Team Building
1. **Founders build first** - Critical for product insights 1. **Founders build first** - Critical for product insights
2. **Hire for adaptability** over pedigree 2. **Hire for adaptability** over pedigree
3. **Evolve role with growth** - Coding time decreases with team size 3. **Evolve role with growth** - Coding time decreases with team size
@ -329,4 +369,4 @@
--- ---
*Research compiled from YouTube transcript, web searches, and Y Combinator resources. Last updated: January 25, 2026* _Research compiled from YouTube transcript, web searches, and Y Combinator resources. Last updated: January 25, 2026_

View File

@ -113,9 +113,7 @@
{ {
"name": "present_files", "name": "present_files",
"args": { "args": {
"filepaths": [ "filepaths": ["/mnt/user-data/outputs/index.html"]
"/mnt/user-data/outputs/index.html"
]
}, },
"id": "present_files:2", "id": "present_files:2",
"type": "tool_call" "type": "tool_call"
@ -159,9 +157,7 @@
}, },
"uploaded_files": [], "uploaded_files": [],
"title": "Interactive 3D Weather Forecast Interface", "title": "Interactive 3D Weather Forecast Interface",
"artifacts": [ "artifacts": ["/mnt/user-data/outputs/index.html"]
"/mnt/user-data/outputs/index.html"
]
}, },
"next": [], "next": [],
"tasks": [], "tasks": [],
@ -201,4 +197,4 @@
"interrupts": [], "interrupts": [],
"checkpoint_id": "1f0fbffa-a7ba-674e-800f-42a5653bc5dc", "checkpoint_id": "1f0fbffa-a7ba-674e-800f-42a5653bc5dc",
"parent_checkpoint_id": "1f0fbffa-9f3c-6e8c-800e-411520ded234" "parent_checkpoint_id": "1f0fbffa-9f3c-6e8c-800e-411520ded234"
} }

View File

@ -1741,9 +1741,7 @@
{ {
"name": "present_files", "name": "present_files",
"args": { "args": {
"filepaths": [ "filepaths": ["/mnt/user-data/outputs/index.html"]
"/mnt/user-data/outputs/index.html"
]
}, },
"id": "present_files:34", "id": "present_files:34",
"type": "tool_call" "type": "tool_call"
@ -1873,4 +1871,4 @@
"interrupts": [], "interrupts": [],
"checkpoint_id": "1f0ff191-247b-6e28-8077-c6ae37cb0bc6", "checkpoint_id": "1f0ff191-247b-6e28-8077-c6ae37cb0bc6",
"parent_checkpoint_id": "1f0ff191-2479-6baa-8076-feaaf42a66ed" "parent_checkpoint_id": "1f0ff191-2479-6baa-8076-feaaf42a66ed"
} }

View File

@ -111,16 +111,16 @@ Open Issues: 196 (ongoing maintenance and feature development)
### Key Metrics ### Key Metrics
| Metric | Value | Assessment | | Metric | Value | Assessment |
|--------|-------|------------| | ------------------ | ------------------ | --------------------------------------------------- |
| GitHub Stars | 19,531 | Exceptional popularity for research framework | | GitHub Stars | 19,531 | Exceptional popularity for research framework |
| Forks | 2,452 | Strong community adoption and potential derivatives | | Forks | 2,452 | Strong community adoption and potential derivatives |
| Contributors | 88 | Healthy open-source development ecosystem | | Contributors | 88 | Healthy open-source development ecosystem |
| Open Issues | 196 | Active maintenance and feature development | | Open Issues | 196 | Active maintenance and feature development |
| Primary Language | Python (1.29MB) | Main development language with extensive libraries | | Primary Language | Python (1.29MB) | Main development language with extensive libraries |
| Secondary Language | TypeScript (503KB) | Modern web UI implementation | | Secondary Language | TypeScript (503KB) | Modern web UI implementation |
| Repository Age | ~9 months | Rapid development and feature expansion | | Repository Age | ~9 months | Rapid development and feature expansion |
| License | MIT | Permissive open-source licensing | | License | MIT | Permissive open-source licensing |
--- ---
@ -128,18 +128,18 @@ Open Issues: 196 (ongoing maintenance and feature development)
### Feature Comparison ### Feature Comparison
| Feature | DeerFlow | OpenAI Deep Research | LangChain OpenDeepResearch | | Feature | DeerFlow | OpenAI Deep Research | LangChain OpenDeepResearch |
|---------|-----------|----------------------|----------------------------| | ------------------------ | --------------- | -------------------- | -------------------------- |
| Multi-Agent Architecture | ✅ | ❌ | ✅ | | Multi-Agent Architecture | ✅ | ❌ | ✅ |
| Local LLM Support | ✅ | ❌ | ✅ | | Local LLM Support | ✅ | ❌ | ✅ |
| MCP Integration | ✅ | ❌ | ❌ | | MCP Integration | ✅ | ❌ | ❌ |
| Web Search Engines | Multiple (5+) | Limited | Limited | | Web Search Engines | Multiple (5+) | Limited | Limited |
| Code Execution | ✅ Python REPL | Limited | ✅ | | Code Execution | ✅ Python REPL | Limited | ✅ |
| Podcast Generation | ✅ | ❌ | ❌ | | Podcast Generation | ✅ | ❌ | ❌ |
| Presentation Creation | ✅ | ❌ | ❌ | | Presentation Creation | ✅ | ❌ | ❌ |
| Private Knowledgebase | ✅ (6+ options) | Limited | Limited | | Private Knowledgebase | ✅ (6+ options) | Limited | Limited |
| Human-in-the-Loop | ✅ | Limited | ✅ | | Human-in-the-Loop | ✅ | Limited | ✅ |
| Open Source | ✅ MIT | ❌ | ✅ Apache 2.0 | | Open Source | ✅ MIT | ❌ | ✅ Apache 2.0 |
### Market Positioning ### Market Positioning
@ -217,6 +217,7 @@ DeerFlow occupies a unique position in the deep research framework landscape by
## Confidence Assessment ## Confidence Assessment
**High Confidence (90%+) Claims:** **High Confidence (90%+) Claims:**
- DeerFlow was created by ByteDance and open-sourced under MIT license in May 2025 - DeerFlow was created by ByteDance and open-sourced under MIT license in May 2025
- The framework implements multi-agent architecture using LangGraph and LangChain - The framework implements multi-agent architecture using LangGraph and LangChain
- Current GitHub metrics: 19,531 stars, 2,452 forks, 88 contributors, 196 open issues - Current GitHub metrics: 19,531 stars, 2,452 forks, 88 contributors, 196 open issues
@ -224,11 +225,13 @@ DeerFlow occupies a unique position in the deep research framework landscape by
- Includes features for podcast generation, presentation creation, and human collaboration - Includes features for podcast generation, presentation creation, and human collaboration
**Medium Confidence (70-89%) Claims:** **Medium Confidence (70-89%) Claims:**
- Specific performance benchmarks compared to proprietary alternatives - Specific performance benchmarks compared to proprietary alternatives
- Detailed breakdown of enterprise adoption rates and use cases - Detailed breakdown of enterprise adoption rates and use cases
- Exact resource requirements for various deployment scenarios - Exact resource requirements for various deployment scenarios
**Lower Confidence (50-69%) Claims:** **Lower Confidence (50-69%) Claims:**
- Future development roadmap beyond DeerFlow 2.0 transition - Future development roadmap beyond DeerFlow 2.0 transition
- Specific enterprise customer implementations and case studies - Specific enterprise customer implementations and case studies
- Detailed comparison with emerging competitors not yet widely documented - Detailed comparison with emerging competitors not yet widely documented
@ -255,4 +258,4 @@ This report was compiled using:
**Report Prepared By:** Github Deep Research by DeerFlow **Report Prepared By:** Github Deep Research by DeerFlow
**Date:** 2026-02-01 **Date:** 2026-02-01
**Report Version:** 1.0 **Report Version:** 1.0
**Status:** Complete **Status:** Complete

View File

@ -14,7 +14,8 @@ type MockThreadSearchResult = Record<string, unknown> & {
}; };
export async function POST(request: Request) { export async function POST(request: Request) {
const body = ((await request.json().catch(() => ({}))) ?? {}) as ThreadSearchRequest; const body = ((await request.json().catch(() => ({}))) ??
{}) as ThreadSearchRequest;
const rawLimit = body.limit; const rawLimit = body.limit;
let limit = 50; let limit = 50;

View File

@ -137,7 +137,10 @@ export default function ChatPage() {
extraHeader={ extraHeader={
isNewThread && <Welcome mode={settings.context.mode} /> isNewThread && <Welcome mode={settings.context.mode} />
} }
disabled={env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" || isUploading} disabled={
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" ||
isUploading
}
onContextChange={(context) => setSettings("context", context)} onContextChange={(context) => setSettings("context", context)}
onSubmit={handleSubmit} onSubmit={handleSubmit}
onStop={handleStop} onStop={handleStop}

View File

@ -19,7 +19,10 @@ export const Checkpoint = ({
...props ...props
}: CheckpointProps) => ( }: CheckpointProps) => (
<div <div
className={cn("flex items-center gap-0.5 text-muted-foreground overflow-hidden", className)} className={cn(
"text-muted-foreground flex items-center gap-0.5 overflow-hidden",
className,
)}
{...props} {...props}
> >
{children} {children}

View File

@ -115,7 +115,7 @@ export const ContextTrigger = ({ children, ...props }: ContextTriggerProps) => {
<HoverCardTrigger asChild> <HoverCardTrigger asChild>
{children ?? ( {children ?? (
<Button type="button" variant="ghost" {...props}> <Button type="button" variant="ghost" {...props}>
<span className="font-medium text-muted-foreground"> <span className="text-muted-foreground font-medium">
{renderedPercent} {renderedPercent}
</span> </span>
<ContextIcon /> <ContextIcon />
@ -163,7 +163,7 @@ export const ContextContentHeader = ({
<> <>
<div className="flex items-center justify-between gap-3 text-xs"> <div className="flex items-center justify-between gap-3 text-xs">
<p>{displayPct}</p> <p>{displayPct}</p>
<p className="font-mono text-muted-foreground"> <p className="text-muted-foreground font-mono">
{used} / {total} {used} / {total}
</p> </p>
</div> </div>
@ -213,8 +213,8 @@ export const ContextContentFooter = ({
return ( return (
<div <div
className={cn( className={cn(
"flex w-full items-center justify-between gap-3 bg-secondary p-3 text-xs", "bg-secondary flex w-full items-center justify-between gap-3 p-3 text-xs",
className className,
)} )}
{...props} {...props}
> >
@ -402,7 +402,7 @@ const TokensWithCost = ({
notation: "compact", notation: "compact",
}).format(tokens)} }).format(tokens)}
{costText ? ( {costText ? (
<span className="ml-2 text-muted-foreground"> {costText}</span> <span className="text-muted-foreground ml-2"> {costText}</span>
) : null} ) : null}
</span> </span>
); );

View File

@ -9,9 +9,9 @@ export type ControlsProps = ComponentProps<typeof ControlsPrimitive>;
export const Controls = ({ className, ...props }: ControlsProps) => ( export const Controls = ({ className, ...props }: ControlsProps) => (
<ControlsPrimitive <ControlsPrimitive
className={cn( className={cn(
"gap-px overflow-hidden rounded-md border bg-card p-1 shadow-none!", "bg-card gap-px overflow-hidden rounded-md border p-1 shadow-none!",
"[&>button]:rounded-md [&>button]:border-none! [&>button]:bg-transparent! [&>button]:hover:bg-secondary!", "[&>button]:hover:bg-secondary! [&>button]:rounded-md [&>button]:border-none! [&>button]:bg-transparent!",
className className,
)} )}
{...props} {...props}
/> />

View File

@ -29,7 +29,7 @@ const Temporary = ({
return ( return (
<BaseEdge <BaseEdge
className="stroke-1 stroke-ring" className="stroke-ring stroke-1"
id={id} id={id}
path={edgePath} path={edgePath}
style={{ style={{
@ -41,13 +41,13 @@ const Temporary = ({
const getHandleCoordsByPosition = ( const getHandleCoordsByPosition = (
node: InternalNode<Node>, node: InternalNode<Node>,
handlePosition: Position handlePosition: Position,
) => { ) => {
// Choose the handle type based on position - Left is for target, Right is for source // Choose the handle type based on position - Left is for target, Right is for source
const handleType = handlePosition === Position.Left ? "target" : "source"; const handleType = handlePosition === Position.Left ? "target" : "source";
const handle = node.internals.handleBounds?.[handleType]?.find( const handle = node.internals.handleBounds?.[handleType]?.find(
(h) => h.position === handlePosition (h) => h.position === handlePosition,
); );
if (!handle) { if (!handle) {
@ -85,7 +85,7 @@ const getHandleCoordsByPosition = (
const getEdgeParams = ( const getEdgeParams = (
source: InternalNode<Node>, source: InternalNode<Node>,
target: InternalNode<Node> target: InternalNode<Node>,
) => { ) => {
const sourcePos = Position.Right; const sourcePos = Position.Right;
const [sx, sy] = getHandleCoordsByPosition(source, sourcePos); const [sx, sy] = getHandleCoordsByPosition(source, sourcePos);
@ -112,7 +112,7 @@ const Animated = ({ id, source, target, markerEnd, style }: EdgeProps) => {
const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams( const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(
sourceNode, sourceNode,
targetNode targetNode,
); );
const [edgePath] = getBezierPath({ const [edgePath] = getBezierPath({

View File

@ -17,7 +17,7 @@ export const Image = ({
alt={props.alt} alt={props.alt}
className={cn( className={cn(
"h-auto max-w-full overflow-hidden rounded-md", "h-auto max-w-full overflow-hidden rounded-md",
props.className props.className,
)} )}
src={`data:${mediaType};base64,${base64}`} src={`data:${mediaType};base64,${base64}`}
/> />

View File

@ -87,7 +87,7 @@ export const Loader = ({ className, size = 16, ...props }: LoaderProps) => (
<div <div
className={cn( className={cn(
"inline-flex animate-spin items-center justify-center", "inline-flex animate-spin items-center justify-center",
className className,
)} )}
{...props} {...props}
> >

View File

@ -22,7 +22,7 @@ export const Node = ({ handles, className, ...props }: NodeProps) => (
<Card <Card
className={cn( className={cn(
"node-container relative size-full h-auto w-sm gap-0 rounded-md p-0", "node-container relative size-full h-auto w-sm gap-0 rounded-md p-0",
className className,
)} )}
{...props} {...props}
> >
@ -36,7 +36,7 @@ export type NodeHeaderProps = ComponentProps<typeof CardHeader>;
export const NodeHeader = ({ className, ...props }: NodeHeaderProps) => ( export const NodeHeader = ({ className, ...props }: NodeHeaderProps) => (
<CardHeader <CardHeader
className={cn("gap-0.5 rounded-t-md border-b bg-secondary p-3!", className)} className={cn("bg-secondary gap-0.5 rounded-t-md border-b p-3!", className)}
{...props} {...props}
/> />
); );
@ -65,7 +65,7 @@ export type NodeFooterProps = ComponentProps<typeof CardFooter>;
export const NodeFooter = ({ className, ...props }: NodeFooterProps) => ( export const NodeFooter = ({ className, ...props }: NodeFooterProps) => (
<CardFooter <CardFooter
className={cn("rounded-b-md border-t bg-secondary p-3!", className)} className={cn("bg-secondary rounded-b-md border-t p-3!", className)}
{...props} {...props}
/> />
); );

View File

@ -7,8 +7,8 @@ type PanelProps = ComponentProps<typeof PanelPrimitive>;
export const Panel = ({ className, ...props }: PanelProps) => ( export const Panel = ({ className, ...props }: PanelProps) => (
<PanelPrimitive <PanelPrimitive
className={cn( className={cn(
"m-4 overflow-hidden rounded-md border bg-card p-1", "bg-card m-4 overflow-hidden rounded-md border p-1",
className className,
)} )}
{...props} {...props}
/> />

View File

@ -1163,7 +1163,8 @@ export const PromptInputSpeechButton = ({
} }
const currentTextareaRef = callbacksRef.current.textareaRef; const currentTextareaRef = callbacksRef.current.textareaRef;
const currentOnTranscriptionChange = callbacksRef.current.onTranscriptionChange; const currentOnTranscriptionChange =
callbacksRef.current.onTranscriptionChange;
if (finalTranscript && currentTextareaRef?.current) { if (finalTranscript && currentTextareaRef?.current) {
const textarea = currentTextareaRef.current; const textarea = currentTextareaRef.current;

View File

@ -36,8 +36,8 @@ export type QueueItemProps = ComponentProps<"li">;
export const QueueItem = ({ className, ...props }: QueueItemProps) => ( export const QueueItem = ({ className, ...props }: QueueItemProps) => (
<li <li
className={cn( className={cn(
"group flex flex-col gap-1 rounded-md px-3 py-1 text-sm transition-colors hover:bg-muted", "group hover:bg-muted flex flex-col gap-1 rounded-md px-3 py-1 text-sm transition-colors",
className className,
)} )}
{...props} {...props}
/> />
@ -58,7 +58,7 @@ export const QueueItemIndicator = ({
completed completed
? "border-muted-foreground/20 bg-muted-foreground/10" ? "border-muted-foreground/20 bg-muted-foreground/10"
: "border-muted-foreground/50", : "border-muted-foreground/50",
className className,
)} )}
{...props} {...props}
/> />
@ -79,7 +79,7 @@ export const QueueItemContent = ({
completed completed
? "text-muted-foreground/50 line-through" ? "text-muted-foreground/50 line-through"
: "text-muted-foreground", : "text-muted-foreground",
className className,
)} )}
{...props} {...props}
/> />
@ -100,7 +100,7 @@ export const QueueItemDescription = ({
completed completed
? "text-muted-foreground/40 line-through" ? "text-muted-foreground/40 line-through"
: "text-muted-foreground", : "text-muted-foreground",
className className,
)} )}
{...props} {...props}
/> />
@ -126,8 +126,8 @@ export const QueueItemAction = ({
}: QueueItemActionProps) => ( }: QueueItemActionProps) => (
<Button <Button
className={cn( className={cn(
"size-auto rounded p-1 text-muted-foreground opacity-0 transition-opacity hover:bg-muted-foreground/10 hover:text-foreground group-hover:opacity-100", "text-muted-foreground hover:bg-muted-foreground/10 hover:text-foreground size-auto rounded p-1 opacity-0 transition-opacity group-hover:opacity-100",
className className,
)} )}
size="icon" size="icon"
type="button" type="button"
@ -169,8 +169,8 @@ export const QueueItemFile = ({
}: QueueItemFileProps) => ( }: QueueItemFileProps) => (
<span <span
className={cn( className={cn(
"flex items-center gap-1 rounded border bg-muted px-2 py-1 text-xs", "bg-muted flex items-center gap-1 rounded border px-2 py-1 text-xs",
className className,
)} )}
{...props} {...props}
> >
@ -186,7 +186,7 @@ export const QueueList = ({
className, className,
...props ...props
}: QueueListProps) => ( }: QueueListProps) => (
<ScrollArea className={cn("-mb-1 mt-2", className)} {...props}> <ScrollArea className={cn("mt-2 -mb-1", className)} {...props}>
<div className="max-h-40 pr-4"> <div className="max-h-40 pr-4">
<ul>{children}</ul> <ul>{children}</ul>
</div> </div>
@ -215,8 +215,8 @@ export const QueueSectionTrigger = ({
<CollapsibleTrigger asChild> <CollapsibleTrigger asChild>
<button <button
className={cn( className={cn(
"group flex w-full items-center justify-between rounded-md bg-muted/40 px-3 py-2 text-left font-medium text-muted-foreground text-sm transition-colors hover:bg-muted", "group bg-muted/40 text-muted-foreground hover:bg-muted flex w-full items-center justify-between rounded-md px-3 py-2 text-left text-sm font-medium transition-colors",
className className,
)} )}
type="button" type="button"
{...props} {...props}
@ -241,7 +241,7 @@ export const QueueSectionLabel = ({
...props ...props
}: QueueSectionLabelProps) => ( }: QueueSectionLabelProps) => (
<span className={cn("flex items-center gap-2", className)} {...props}> <span className={cn("flex items-center gap-2", className)} {...props}>
<ChevronDownIcon className="group-data-[state=closed]:-rotate-90 size-4 transition-transform" /> <ChevronDownIcon className="size-4 transition-transform group-data-[state=closed]:-rotate-90" />
{icon} {icon}
<span> <span>
{count} {label} {count} {label}
@ -266,8 +266,8 @@ export type QueueProps = ComponentProps<"div">;
export const Queue = ({ className, ...props }: QueueProps) => ( export const Queue = ({ className, ...props }: QueueProps) => (
<div <div
className={cn( className={cn(
"flex flex-col gap-2 rounded-xl border border-border bg-background px-3 pt-2 pb-2 shadow-xs", "border-border bg-background flex flex-col gap-2 rounded-xl border px-3 pt-2 pb-2 shadow-xs",
className className,
)} )}
{...props} {...props}
/> />

View File

@ -108,10 +108,12 @@ export const Reasoning = memo(
</Collapsible> </Collapsible>
</ReasoningContext.Provider> </ReasoningContext.Provider>
); );
} },
); );
export type ReasoningTriggerProps = ComponentProps<typeof CollapsibleTrigger> & { export type ReasoningTriggerProps = ComponentProps<
typeof CollapsibleTrigger
> & {
getThinkingMessage?: (isStreaming: boolean, duration?: number) => ReactNode; getThinkingMessage?: (isStreaming: boolean, duration?: number) => ReactNode;
}; };
@ -126,14 +128,19 @@ const defaultGetThinkingMessage = (isStreaming: boolean, duration?: number) => {
}; };
export const ReasoningTrigger = memo( export const ReasoningTrigger = memo(
({ className, children, getThinkingMessage = defaultGetThinkingMessage, ...props }: ReasoningTriggerProps) => { ({
className,
children,
getThinkingMessage = defaultGetThinkingMessage,
...props
}: ReasoningTriggerProps) => {
const { isStreaming, isOpen, duration } = useReasoning(); const { isStreaming, isOpen, duration } = useReasoning();
return ( return (
<CollapsibleTrigger <CollapsibleTrigger
className={cn( className={cn(
"flex w-full items-center gap-2 text-muted-foreground text-sm transition-colors hover:text-foreground", "text-muted-foreground hover:text-foreground flex w-full items-center gap-2 text-sm transition-colors",
className className,
)} )}
{...props} {...props}
> >
@ -144,14 +151,14 @@ export const ReasoningTrigger = memo(
<ChevronDownIcon <ChevronDownIcon
className={cn( className={cn(
"size-4 transition-transform", "size-4 transition-transform",
isOpen ? "rotate-180" : "rotate-0" isOpen ? "rotate-180" : "rotate-0",
)} )}
/> />
</> </>
)} )}
</CollapsibleTrigger> </CollapsibleTrigger>
); );
} },
); );
export type ReasoningContentProps = ComponentProps< export type ReasoningContentProps = ComponentProps<
@ -165,14 +172,14 @@ export const ReasoningContent = memo(
<CollapsibleContent <CollapsibleContent
className={cn( className={cn(
"mt-4 text-sm", "mt-4 text-sm",
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-muted-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in", "data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-muted-foreground data-[state=closed]:animate-out data-[state=open]:animate-in outline-none",
className className,
)} )}
{...props} {...props}
> >
<Streamdown {...props}>{children}</Streamdown> <Streamdown {...props}>{children}</Streamdown>
</CollapsibleContent> </CollapsibleContent>
) ),
); );
Reasoning.displayName = "Reasoning"; Reasoning.displayName = "Reasoning";

View File

@ -26,12 +26,12 @@ const ShimmerComponent = ({
spread = 2, spread = 2,
}: TextShimmerProps) => { }: TextShimmerProps) => {
const MotionComponent = motion.create( const MotionComponent = motion.create(
Component as keyof JSX.IntrinsicElements Component as keyof JSX.IntrinsicElements,
); );
const dynamicSpread = useMemo( const dynamicSpread = useMemo(
() => (children?.length ?? 0) * spread, () => (children?.length ?? 0) * spread,
[children, spread] [children, spread],
); );
return ( return (
@ -39,8 +39,8 @@ const ShimmerComponent = ({
animate={{ backgroundPosition: "0% center" }} animate={{ backgroundPosition: "0% center" }}
className={cn( className={cn(
"relative inline-block bg-[length:250%_100%,auto] bg-clip-text text-transparent", "relative inline-block bg-[length:250%_100%,auto] bg-clip-text text-transparent",
"[--bg:linear-gradient(90deg,#0000_calc(50%-var(--spread)),var(--color-background),#0000_calc(50%+var(--spread)))] [background-repeat:no-repeat,padding-box]", "[background-repeat:no-repeat,padding-box] [--bg:linear-gradient(90deg,#0000_calc(50%-var(--spread)),var(--color-background),#0000_calc(50%+var(--spread)))]",
className className,
)} )}
initial={{ backgroundPosition: "100% center" }} initial={{ backgroundPosition: "100% center" }}
style={ style={

View File

@ -13,7 +13,7 @@ export type SourcesProps = ComponentProps<"div">;
export const Sources = ({ className, ...props }: SourcesProps) => ( export const Sources = ({ className, ...props }: SourcesProps) => (
<Collapsible <Collapsible
className={cn("not-prose mb-4 text-primary text-xs", className)} className={cn("not-prose text-primary mb-4 text-xs", className)}
{...props} {...props}
/> />
); );
@ -50,8 +50,8 @@ export const SourcesContent = ({
<CollapsibleContent <CollapsibleContent
className={cn( className={cn(
"mt-3 flex w-fit flex-col gap-2", "mt-3 flex w-fit flex-col gap-2",
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 outline-none data-[state=closed]:animate-out data-[state=open]:animate-in", "data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 data-[state=closed]:animate-out data-[state=open]:animate-in outline-none",
className className,
)} )}
{...props} {...props}
/> />

View File

@ -18,8 +18,8 @@ export const TaskItemFile = ({
}: TaskItemFileProps) => ( }: TaskItemFileProps) => (
<div <div
className={cn( className={cn(
"inline-flex items-center gap-1 rounded-md border bg-secondary px-1.5 py-0.5 text-foreground text-xs", "bg-secondary text-foreground inline-flex items-center gap-1 rounded-md border px-1.5 py-0.5 text-xs",
className className,
)} )}
{...props} {...props}
> >
@ -57,7 +57,7 @@ export const TaskTrigger = ({
}: TaskTriggerProps) => ( }: TaskTriggerProps) => (
<CollapsibleTrigger asChild className={cn("group", className)} {...props}> <CollapsibleTrigger asChild className={cn("group", className)} {...props}>
{children ?? ( {children ?? (
<div className="flex w-full cursor-pointer items-center gap-2 text-muted-foreground text-sm transition-colors hover:text-foreground"> <div className="text-muted-foreground hover:text-foreground flex w-full cursor-pointer items-center gap-2 text-sm transition-colors">
<SearchIcon className="size-4" /> <SearchIcon className="size-4" />
<p className="text-sm">{title}</p> <p className="text-sm">{title}</p>
<ChevronDownIcon className="size-4 transition-transform group-data-[state=open]:rotate-180" /> <ChevronDownIcon className="size-4 transition-transform group-data-[state=open]:rotate-180" />
@ -75,12 +75,12 @@ export const TaskContent = ({
}: TaskContentProps) => ( }: TaskContentProps) => (
<CollapsibleContent <CollapsibleContent
className={cn( className={cn(
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-popover-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in", "data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-popover-foreground data-[state=closed]:animate-out data-[state=open]:animate-in outline-none",
className className,
)} )}
{...props} {...props}
> >
<div className="mt-4 space-y-2 border-muted border-l-2 pl-4"> <div className="border-muted mt-4 space-y-2 border-l-2 pl-4">
{children} {children}
</div> </div>
</CollapsibleContent> </CollapsibleContent>

View File

@ -7,8 +7,8 @@ type ToolbarProps = ComponentProps<typeof NodeToolbar>;
export const Toolbar = ({ className, ...props }: ToolbarProps) => ( export const Toolbar = ({ className, ...props }: ToolbarProps) => (
<NodeToolbar <NodeToolbar
className={cn( className={cn(
"flex items-center gap-1 rounded-sm border bg-background p-1.5", "bg-background flex items-center gap-1 rounded-sm border p-1.5",
className className,
)} )}
position={Position.Bottom} position={Position.Bottom}
{...props} {...props}

View File

@ -66,8 +66,8 @@ export const WebPreview = ({
<WebPreviewContext.Provider value={contextValue}> <WebPreviewContext.Provider value={contextValue}>
<div <div
className={cn( className={cn(
"flex size-full flex-col rounded-lg border bg-card", "bg-card flex size-full flex-col rounded-lg border",
className className,
)} )}
{...props} {...props}
> >
@ -107,7 +107,7 @@ export const WebPreviewNavigationButton = ({
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
className="h-8 w-8 p-0 hover:text-foreground" className="hover:text-foreground h-8 w-8 p-0"
disabled={disabled} disabled={disabled}
onClick={onClick} onClick={onClick}
size="sm" size="sm"
@ -209,21 +209,21 @@ export const WebPreviewConsole = ({
return ( return (
<Collapsible <Collapsible
className={cn("border-t bg-muted/50 font-mono text-sm", className)} className={cn("bg-muted/50 border-t font-mono text-sm", className)}
onOpenChange={setConsoleOpen} onOpenChange={setConsoleOpen}
open={consoleOpen} open={consoleOpen}
{...props} {...props}
> >
<CollapsibleTrigger asChild> <CollapsibleTrigger asChild>
<Button <Button
className="flex w-full items-center justify-between p-4 text-left font-medium hover:bg-muted/50" className="hover:bg-muted/50 flex w-full items-center justify-between p-4 text-left font-medium"
variant="ghost" variant="ghost"
> >
Console Console
<ChevronDownIcon <ChevronDownIcon
className={cn( className={cn(
"h-4 w-4 transition-transform duration-200", "h-4 w-4 transition-transform duration-200",
consoleOpen && "rotate-180" consoleOpen && "rotate-180",
)} )}
/> />
</Button> </Button>
@ -231,7 +231,7 @@ export const WebPreviewConsole = ({
<CollapsibleContent <CollapsibleContent
className={cn( className={cn(
"px-4 pb-4", "px-4 pb-4",
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 outline-none data-[state=closed]:animate-out data-[state=open]:animate-in" "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=open]:animate-in outline-none",
)} )}
> >
<div className="max-h-48 space-y-1 overflow-y-auto"> <div className="max-h-48 space-y-1 overflow-y-auto">
@ -244,7 +244,7 @@ export const WebPreviewConsole = ({
"text-xs", "text-xs",
log.level === "error" && "text-destructive", log.level === "error" && "text-destructive",
log.level === "warn" && "text-yellow-600", log.level === "warn" && "text-yellow-600",
log.level === "log" && "text-foreground" log.level === "log" && "text-foreground",
)} )}
key={`${log.timestamp.getTime()}-${index}`} key={`${log.timestamp.getTime()}-${index}`}
> >

View File

@ -1,7 +1,7 @@
import * as React from "react" import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const alertVariants = cva( const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
@ -16,8 +16,8 @@ const alertVariants = cva(
defaultVariants: { defaultVariants: {
variant: "default", variant: "default",
}, },
} },
) );
function Alert({ function Alert({
className, className,
@ -31,7 +31,7 @@ function Alert({
className={cn(alertVariants({ variant }), className)} className={cn(alertVariants({ variant }), className)}
{...props} {...props}
/> />
) );
} }
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
@ -40,11 +40,11 @@ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
data-slot="alert-title" data-slot="alert-title"
className={cn( className={cn(
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight", "col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function AlertDescription({ function AlertDescription({
@ -56,11 +56,11 @@ function AlertDescription({
data-slot="alert-description" data-slot="alert-description"
className={cn( className={cn(
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed", "text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
export { Alert, AlertTitle, AlertDescription } export { Alert, AlertTitle, AlertDescription };

View File

@ -1,12 +1,12 @@
"use client" "use client";
import React, { memo } from "react" import React, { memo } from "react";
interface AuroraTextProps { interface AuroraTextProps {
children: React.ReactNode children: React.ReactNode;
className?: string className?: string;
colors?: string[] colors?: string[];
speed?: number speed?: number;
} }
export const AuroraText = memo( export const AuroraText = memo(
@ -23,7 +23,7 @@ export const AuroraText = memo(
WebkitBackgroundClip: "text", WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent", WebkitTextFillColor: "transparent",
animationDuration: `${10 / speed}s`, animationDuration: `${10 / speed}s`,
} };
return ( return (
<span className={`relative inline-block ${className}`}> <span className={`relative inline-block ${className}`}>
@ -36,8 +36,8 @@ export const AuroraText = memo(
{children} {children}
</span> </span>
</span> </span>
) );
} },
) );
AuroraText.displayName = "AuroraText" AuroraText.displayName = "AuroraText";

View File

@ -1,9 +1,9 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import * as AvatarPrimitive from "@radix-ui/react-avatar" import * as AvatarPrimitive from "@radix-ui/react-avatar";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Avatar({ function Avatar({
className, className,
@ -14,11 +14,11 @@ function Avatar({
data-slot="avatar" data-slot="avatar"
className={cn( className={cn(
"relative flex size-8 shrink-0 overflow-hidden rounded-full", "relative flex size-8 shrink-0 overflow-hidden rounded-full",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function AvatarImage({ function AvatarImage({
@ -31,7 +31,7 @@ function AvatarImage({
className={cn("aspect-square size-full", className)} className={cn("aspect-square size-full", className)}
{...props} {...props}
/> />
) );
} }
function AvatarFallback({ function AvatarFallback({
@ -43,11 +43,11 @@ function AvatarFallback({
data-slot="avatar-fallback" data-slot="avatar-fallback"
className={cn( className={cn(
"bg-muted flex size-full items-center justify-center rounded-full", "bg-muted flex size-full items-center justify-center rounded-full",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
export { Avatar, AvatarImage, AvatarFallback } export { Avatar, AvatarImage, AvatarFallback };

View File

@ -1,8 +1,8 @@
import * as React from "react" import * as React from "react";
import { Slot } from "@radix-ui/react-slot" import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const badgeVariants = cva( const badgeVariants = cva(
"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", "inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
@ -22,8 +22,8 @@ const badgeVariants = cva(
defaultVariants: { defaultVariants: {
variant: "default", variant: "default",
}, },
} },
) );
function Badge({ function Badge({
className, className,
@ -32,7 +32,7 @@ function Badge({
...props ...props
}: React.ComponentProps<"span"> & }: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) { VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span" const Comp = asChild ? Slot : "span";
return ( return (
<Comp <Comp
@ -40,7 +40,7 @@ function Badge({
className={cn(badgeVariants({ variant }), className)} className={cn(badgeVariants({ variant }), className)}
{...props} {...props}
/> />
) );
} }
export { Badge, badgeVariants } export { Badge, badgeVariants };

View File

@ -1,11 +1,11 @@
import * as React from "react" import * as React from "react";
import { Slot } from "@radix-ui/react-slot" import { Slot } from "@radix-ui/react-slot";
import { ChevronRight, MoreHorizontal } from "lucide-react" import { ChevronRight, MoreHorizontal } from "lucide-react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} /> return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />;
} }
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) { function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
@ -14,11 +14,11 @@ function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
data-slot="breadcrumb-list" data-slot="breadcrumb-list"
className={cn( className={cn(
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5", "text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) { function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
@ -28,7 +28,7 @@ function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
className={cn("inline-flex items-center gap-1.5", className)} className={cn("inline-flex items-center gap-1.5", className)}
{...props} {...props}
/> />
) );
} }
function BreadcrumbLink({ function BreadcrumbLink({
@ -36,9 +36,9 @@ function BreadcrumbLink({
className, className,
...props ...props
}: React.ComponentProps<"a"> & { }: React.ComponentProps<"a"> & {
asChild?: boolean asChild?: boolean;
}) { }) {
const Comp = asChild ? Slot : "a" const Comp = asChild ? Slot : "a";
return ( return (
<Comp <Comp
@ -46,7 +46,7 @@ function BreadcrumbLink({
className={cn("hover:text-foreground transition-colors", className)} className={cn("hover:text-foreground transition-colors", className)}
{...props} {...props}
/> />
) );
} }
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) { function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
@ -59,7 +59,7 @@ function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
className={cn("text-foreground font-normal", className)} className={cn("text-foreground font-normal", className)}
{...props} {...props}
/> />
) );
} }
function BreadcrumbSeparator({ function BreadcrumbSeparator({
@ -77,7 +77,7 @@ function BreadcrumbSeparator({
> >
{children ?? <ChevronRight />} {children ?? <ChevronRight />}
</li> </li>
) );
} }
function BreadcrumbEllipsis({ function BreadcrumbEllipsis({
@ -95,7 +95,7 @@ function BreadcrumbEllipsis({
<MoreHorizontal className="size-4" /> <MoreHorizontal className="size-4" />
<span className="sr-only">More</span> <span className="sr-only">More</span>
</span> </span>
) );
} }
export { export {
@ -106,4 +106,4 @@ export {
BreadcrumbPage, BreadcrumbPage,
BreadcrumbSeparator, BreadcrumbSeparator,
BreadcrumbEllipsis, BreadcrumbEllipsis,
} };

View File

@ -1,8 +1,8 @@
import { Slot } from "@radix-ui/react-slot" import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
import { Separator } from "@/components/ui/separator" import { Separator } from "@/components/ui/separator";
const buttonGroupVariants = cva( const buttonGroupVariants = cva(
"flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2", "flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2",
@ -18,8 +18,8 @@ const buttonGroupVariants = cva(
defaultVariants: { defaultVariants: {
orientation: "horizontal", orientation: "horizontal",
}, },
} },
) );
function ButtonGroup({ function ButtonGroup({
className, className,
@ -34,7 +34,7 @@ function ButtonGroup({
className={cn(buttonGroupVariants({ orientation }), className)} className={cn(buttonGroupVariants({ orientation }), className)}
{...props} {...props}
/> />
) );
} }
function ButtonGroupText({ function ButtonGroupText({
@ -42,19 +42,19 @@ function ButtonGroupText({
asChild = false, asChild = false,
...props ...props
}: React.ComponentProps<"div"> & { }: React.ComponentProps<"div"> & {
asChild?: boolean asChild?: boolean;
}) { }) {
const Comp = asChild ? Slot : "div" const Comp = asChild ? Slot : "div";
return ( return (
<Comp <Comp
className={cn( className={cn(
"bg-muted flex items-center gap-2 rounded-md border px-4 text-sm font-medium shadow-xs [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4", "bg-muted flex items-center gap-2 rounded-md border px-4 text-sm font-medium shadow-xs [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function ButtonGroupSeparator({ function ButtonGroupSeparator({
@ -68,11 +68,11 @@ function ButtonGroupSeparator({
orientation={orientation} orientation={orientation}
className={cn( className={cn(
"bg-input relative !m-0 self-stretch data-[orientation=vertical]:h-auto", "bg-input relative !m-0 self-stretch data-[orientation=vertical]:h-auto",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
export { export {
@ -80,4 +80,4 @@ export {
ButtonGroupSeparator, ButtonGroupSeparator,
ButtonGroupText, ButtonGroupText,
buttonGroupVariants, buttonGroupVariants,
} };

View File

@ -1,6 +1,6 @@
import * as React from "react" import * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Card({ className, ...props }: React.ComponentProps<"div">) { function Card({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
@ -8,11 +8,11 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
data-slot="card" data-slot="card"
className={cn( className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm", "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function CardHeader({ className, ...props }: React.ComponentProps<"div">) { function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
@ -21,11 +21,11 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
data-slot="card-header" data-slot="card-header"
className={cn( className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6", "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function CardTitle({ className, ...props }: React.ComponentProps<"div">) { function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
@ -35,7 +35,7 @@ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
className={cn("leading-none font-semibold", className)} className={cn("leading-none font-semibold", className)}
{...props} {...props}
/> />
) );
} }
function CardDescription({ className, ...props }: React.ComponentProps<"div">) { function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
@ -45,7 +45,7 @@ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
className={cn("text-muted-foreground text-sm", className)} className={cn("text-muted-foreground text-sm", className)}
{...props} {...props}
/> />
) );
} }
function CardAction({ className, ...props }: React.ComponentProps<"div">) { function CardAction({ className, ...props }: React.ComponentProps<"div">) {
@ -54,11 +54,11 @@ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
data-slot="card-action" data-slot="card-action"
className={cn( className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end", "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function CardContent({ className, ...props }: React.ComponentProps<"div">) { function CardContent({ className, ...props }: React.ComponentProps<"div">) {
@ -68,7 +68,7 @@ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
className={cn("px-6", className)} className={cn("px-6", className)}
{...props} {...props}
/> />
) );
} }
function CardFooter({ className, ...props }: React.ComponentProps<"div">) { function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
@ -78,7 +78,7 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
className={cn("flex items-center px-6 [.border-t]:pt-6", className)} className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props} {...props}
/> />
) );
} }
export { export {
@ -89,4 +89,4 @@ export {
CardAction, CardAction,
CardDescription, CardDescription,
CardContent, CardContent,
} };

View File

@ -1,45 +1,45 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import useEmblaCarousel, { import useEmblaCarousel, {
type UseEmblaCarouselType, type UseEmblaCarouselType,
} from "embla-carousel-react" } from "embla-carousel-react";
import { ArrowLeft, ArrowRight } from "lucide-react" import { ArrowLeft, ArrowRight } from "lucide-react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button";
type CarouselApi = UseEmblaCarouselType[1] type CarouselApi = UseEmblaCarouselType[1];
type UseCarouselParameters = Parameters<typeof useEmblaCarousel> type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
type CarouselOptions = UseCarouselParameters[0] type CarouselOptions = UseCarouselParameters[0];
type CarouselPlugin = UseCarouselParameters[1] type CarouselPlugin = UseCarouselParameters[1];
type CarouselProps = { type CarouselProps = {
opts?: CarouselOptions opts?: CarouselOptions;
plugins?: CarouselPlugin plugins?: CarouselPlugin;
orientation?: "horizontal" | "vertical" orientation?: "horizontal" | "vertical";
setApi?: (api: CarouselApi) => void setApi?: (api: CarouselApi) => void;
} };
type CarouselContextProps = { type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0] carouselRef: ReturnType<typeof useEmblaCarousel>[0];
api: ReturnType<typeof useEmblaCarousel>[1] api: ReturnType<typeof useEmblaCarousel>[1];
scrollPrev: () => void scrollPrev: () => void;
scrollNext: () => void scrollNext: () => void;
canScrollPrev: boolean canScrollPrev: boolean;
canScrollNext: boolean canScrollNext: boolean;
} & CarouselProps } & CarouselProps;
const CarouselContext = React.createContext<CarouselContextProps | null>(null) const CarouselContext = React.createContext<CarouselContextProps | null>(null);
function useCarousel() { function useCarousel() {
const context = React.useContext(CarouselContext) const context = React.useContext(CarouselContext);
if (!context) { if (!context) {
throw new Error("useCarousel must be used within a <Carousel />") throw new Error("useCarousel must be used within a <Carousel />");
} }
return context return context;
} }
function Carousel({ function Carousel({
@ -56,53 +56,53 @@ function Carousel({
...opts, ...opts,
axis: orientation === "horizontal" ? "x" : "y", axis: orientation === "horizontal" ? "x" : "y",
}, },
plugins plugins,
) );
const [canScrollPrev, setCanScrollPrev] = React.useState(false) const [canScrollPrev, setCanScrollPrev] = React.useState(false);
const [canScrollNext, setCanScrollNext] = React.useState(false) const [canScrollNext, setCanScrollNext] = React.useState(false);
const onSelect = React.useCallback((api: CarouselApi) => { const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) return if (!api) return;
setCanScrollPrev(api.canScrollPrev()) setCanScrollPrev(api.canScrollPrev());
setCanScrollNext(api.canScrollNext()) setCanScrollNext(api.canScrollNext());
}, []) }, []);
const scrollPrev = React.useCallback(() => { const scrollPrev = React.useCallback(() => {
api?.scrollPrev() api?.scrollPrev();
}, [api]) }, [api]);
const scrollNext = React.useCallback(() => { const scrollNext = React.useCallback(() => {
api?.scrollNext() api?.scrollNext();
}, [api]) }, [api]);
const handleKeyDown = React.useCallback( const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => { (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") { if (event.key === "ArrowLeft") {
event.preventDefault() event.preventDefault();
scrollPrev() scrollPrev();
} else if (event.key === "ArrowRight") { } else if (event.key === "ArrowRight") {
event.preventDefault() event.preventDefault();
scrollNext() scrollNext();
} }
}, },
[scrollPrev, scrollNext] [scrollPrev, scrollNext],
) );
React.useEffect(() => { React.useEffect(() => {
if (!api || !setApi) return if (!api || !setApi) return;
setApi(api) setApi(api);
}, [api, setApi]) }, [api, setApi]);
React.useEffect(() => { React.useEffect(() => {
if (!api) return if (!api) return;
onSelect(api) onSelect(api);
api.on("reInit", onSelect) api.on("reInit", onSelect);
api.on("select", onSelect) api.on("select", onSelect);
return () => { return () => {
api?.off("select", onSelect) api?.off("select", onSelect);
} };
}, [api, onSelect]) }, [api, onSelect]);
return ( return (
<CarouselContext.Provider <CarouselContext.Provider
@ -129,11 +129,11 @@ function Carousel({
{children} {children}
</div> </div>
</CarouselContext.Provider> </CarouselContext.Provider>
) );
} }
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) { function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
const { carouselRef, orientation } = useCarousel() const { carouselRef, orientation } = useCarousel();
return ( return (
<div <div
@ -145,16 +145,16 @@ function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
className={cn( className={cn(
"flex", "flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col", orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
className className,
)} )}
{...props} {...props}
/> />
</div> </div>
) );
} }
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) { function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
const { orientation } = useCarousel() const { orientation } = useCarousel();
return ( return (
<div <div
@ -164,11 +164,11 @@ function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
className={cn( className={cn(
"min-w-0 shrink-0 grow-0 basis-full", "min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4", orientation === "horizontal" ? "pl-4" : "pt-4",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function CarouselPrevious({ function CarouselPrevious({
@ -177,7 +177,7 @@ function CarouselPrevious({
size = "icon", size = "icon",
...props ...props
}: React.ComponentProps<typeof Button>) { }: React.ComponentProps<typeof Button>) {
const { orientation, scrollPrev, canScrollPrev } = useCarousel() const { orientation, scrollPrev, canScrollPrev } = useCarousel();
return ( return (
<Button <Button
@ -189,7 +189,7 @@ function CarouselPrevious({
orientation === "horizontal" orientation === "horizontal"
? "top-1/2 -left-12 -translate-y-1/2" ? "top-1/2 -left-12 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90", : "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className className,
)} )}
disabled={!canScrollPrev} disabled={!canScrollPrev}
onClick={scrollPrev} onClick={scrollPrev}
@ -198,7 +198,7 @@ function CarouselPrevious({
<ArrowLeft /> <ArrowLeft />
<span className="sr-only">Previous slide</span> <span className="sr-only">Previous slide</span>
</Button> </Button>
) );
} }
function CarouselNext({ function CarouselNext({
@ -207,7 +207,7 @@ function CarouselNext({
size = "icon", size = "icon",
...props ...props
}: React.ComponentProps<typeof Button>) { }: React.ComponentProps<typeof Button>) {
const { orientation, scrollNext, canScrollNext } = useCarousel() const { orientation, scrollNext, canScrollNext } = useCarousel();
return ( return (
<Button <Button
@ -219,7 +219,7 @@ function CarouselNext({
orientation === "horizontal" orientation === "horizontal"
? "top-1/2 -right-12 -translate-y-1/2" ? "top-1/2 -right-12 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90", : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className className,
)} )}
disabled={!canScrollNext} disabled={!canScrollNext}
onClick={scrollNext} onClick={scrollNext}
@ -228,7 +228,7 @@ function CarouselNext({
<ArrowRight /> <ArrowRight />
<span className="sr-only">Next slide</span> <span className="sr-only">Next slide</span>
</Button> </Button>
) );
} }
export { export {
@ -238,4 +238,4 @@ export {
CarouselItem, CarouselItem,
CarouselPrevious, CarouselPrevious,
CarouselNext, CarouselNext,
} };

View File

@ -1,17 +1,17 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import { Command as CommandPrimitive } from "cmdk" import { Command as CommandPrimitive } from "cmdk";
import { SearchIcon } from "lucide-react" import { SearchIcon } from "lucide-react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogDescription, DialogDescription,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog" } from "@/components/ui/dialog";
function Command({ function Command({
className, className,
@ -22,11 +22,11 @@ function Command({
data-slot="command" data-slot="command"
className={cn( className={cn(
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md", "bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function CommandDialog({ function CommandDialog({
@ -37,10 +37,10 @@ function CommandDialog({
showCloseButton = true, showCloseButton = true,
...props ...props
}: React.ComponentProps<typeof Dialog> & { }: React.ComponentProps<typeof Dialog> & {
title?: string title?: string;
description?: string description?: string;
className?: string className?: string;
showCloseButton?: boolean showCloseButton?: boolean;
}) { }) {
return ( return (
<Dialog {...props}> <Dialog {...props}>
@ -57,7 +57,7 @@ function CommandDialog({
</Command> </Command>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
) );
} }
function CommandInput({ function CommandInput({
@ -74,12 +74,12 @@ function CommandInput({
data-slot="command-input" data-slot="command-input"
className={cn( className={cn(
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50", "placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
className className,
)} )}
{...props} {...props}
/> />
</div> </div>
) );
} }
function CommandList({ function CommandList({
@ -91,11 +91,11 @@ function CommandList({
data-slot="command-list" data-slot="command-list"
className={cn( className={cn(
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto", "max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function CommandEmpty({ function CommandEmpty({
@ -107,7 +107,7 @@ function CommandEmpty({
className="py-6 text-center text-sm" className="py-6 text-center text-sm"
{...props} {...props}
/> />
) );
} }
function CommandGroup({ function CommandGroup({
@ -119,11 +119,11 @@ function CommandGroup({
data-slot="command-group" data-slot="command-group"
className={cn( className={cn(
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium", "text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function CommandSeparator({ function CommandSeparator({
@ -136,7 +136,7 @@ function CommandSeparator({
className={cn("bg-border -mx-1 h-px", className)} className={cn("bg-border -mx-1 h-px", className)}
{...props} {...props}
/> />
) );
} }
function CommandItem({ function CommandItem({
@ -148,11 +148,11 @@ function CommandItem({
data-slot="command-item" data-slot="command-item"
className={cn( className={cn(
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function CommandShortcut({ function CommandShortcut({
@ -164,11 +164,11 @@ function CommandShortcut({
data-slot="command-shortcut" data-slot="command-shortcut"
className={cn( className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest", "text-muted-foreground ml-auto text-xs tracking-widest",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
export { export {
@ -181,4 +181,4 @@ export {
CommandItem, CommandItem,
CommandShortcut, CommandShortcut,
CommandSeparator, CommandSeparator,
} };

View File

@ -1,33 +1,33 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog" import * as DialogPrimitive from "@radix-ui/react-dialog";
import { XIcon } from "lucide-react" import { XIcon } from "lucide-react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Dialog({ function Dialog({
...props ...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) { }: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} /> return <DialogPrimitive.Root data-slot="dialog" {...props} />;
} }
function DialogTrigger({ function DialogTrigger({
...props ...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) { }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} /> return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
} }
function DialogPortal({ function DialogPortal({
...props ...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) { }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} /> return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
} }
function DialogClose({ function DialogClose({
...props ...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) { }: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} /> return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
} }
function DialogOverlay({ function DialogOverlay({
@ -39,11 +39,11 @@ function DialogOverlay({
data-slot="dialog-overlay" data-slot="dialog-overlay"
className={cn( className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50", "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function DialogContent({ function DialogContent({
@ -52,7 +52,7 @@ function DialogContent({
showCloseButton = true, showCloseButton = true,
...props ...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & { }: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean showCloseButton?: boolean;
}) { }) {
return ( return (
<DialogPortal data-slot="dialog-portal"> <DialogPortal data-slot="dialog-portal">
@ -61,7 +61,7 @@ function DialogContent({
data-slot="dialog-content" data-slot="dialog-content"
className={cn( className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg", "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
className className,
)} )}
{...props} {...props}
> >
@ -77,7 +77,7 @@ function DialogContent({
)} )}
</DialogPrimitive.Content> </DialogPrimitive.Content>
</DialogPortal> </DialogPortal>
) );
} }
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
@ -87,7 +87,7 @@ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
className={cn("flex flex-col gap-2 text-center sm:text-left", className)} className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props} {...props}
/> />
) );
} }
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
@ -96,11 +96,11 @@ function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
data-slot="dialog-footer" data-slot="dialog-footer"
className={cn( className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function DialogTitle({ function DialogTitle({
@ -113,7 +113,7 @@ function DialogTitle({
className={cn("text-lg leading-none font-semibold", className)} className={cn("text-lg leading-none font-semibold", className)}
{...props} {...props}
/> />
) );
} }
function DialogDescription({ function DialogDescription({
@ -126,7 +126,7 @@ function DialogDescription({
className={cn("text-muted-foreground text-sm", className)} className={cn("text-muted-foreground text-sm", className)}
{...props} {...props}
/> />
) );
} }
export { export {
@ -140,4 +140,4 @@ export {
DialogPortal, DialogPortal,
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} };

View File

@ -1,15 +1,15 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function DropdownMenu({ function DropdownMenu({
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} /> return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
} }
function DropdownMenuPortal({ function DropdownMenuPortal({
@ -17,7 +17,7 @@ function DropdownMenuPortal({
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return ( return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} /> <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
) );
} }
function DropdownMenuTrigger({ function DropdownMenuTrigger({
@ -28,7 +28,7 @@ function DropdownMenuTrigger({
data-slot="dropdown-menu-trigger" data-slot="dropdown-menu-trigger"
{...props} {...props}
/> />
) );
} }
function DropdownMenuContent({ function DropdownMenuContent({
@ -43,12 +43,12 @@ function DropdownMenuContent({
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md", "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className className,
)} )}
{...props} {...props}
/> />
</DropdownMenuPrimitive.Portal> </DropdownMenuPrimitive.Portal>
) );
} }
function DropdownMenuGroup({ function DropdownMenuGroup({
@ -56,7 +56,7 @@ function DropdownMenuGroup({
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return ( return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} /> <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
) );
} }
function DropdownMenuItem({ function DropdownMenuItem({
@ -65,8 +65,8 @@ function DropdownMenuItem({
variant = "default", variant = "default",
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & { }: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean inset?: boolean;
variant?: "default" | "destructive" variant?: "default" | "destructive";
}) { }) {
return ( return (
<DropdownMenuPrimitive.Item <DropdownMenuPrimitive.Item
@ -75,11 +75,11 @@ function DropdownMenuItem({
data-variant={variant} data-variant={variant}
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function DropdownMenuCheckboxItem({ function DropdownMenuCheckboxItem({
@ -93,7 +93,7 @@ function DropdownMenuCheckboxItem({
data-slot="dropdown-menu-checkbox-item" data-slot="dropdown-menu-checkbox-item"
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className,
)} )}
checked={checked} checked={checked}
{...props} {...props}
@ -105,7 +105,7 @@ function DropdownMenuCheckboxItem({
</span> </span>
{children} {children}
</DropdownMenuPrimitive.CheckboxItem> </DropdownMenuPrimitive.CheckboxItem>
) );
} }
function DropdownMenuRadioGroup({ function DropdownMenuRadioGroup({
@ -116,7 +116,7 @@ function DropdownMenuRadioGroup({
data-slot="dropdown-menu-radio-group" data-slot="dropdown-menu-radio-group"
{...props} {...props}
/> />
) );
} }
function DropdownMenuRadioItem({ function DropdownMenuRadioItem({
@ -129,7 +129,7 @@ function DropdownMenuRadioItem({
data-slot="dropdown-menu-radio-item" data-slot="dropdown-menu-radio-item"
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className,
)} )}
{...props} {...props}
> >
@ -140,7 +140,7 @@ function DropdownMenuRadioItem({
</span> </span>
{children} {children}
</DropdownMenuPrimitive.RadioItem> </DropdownMenuPrimitive.RadioItem>
) );
} }
function DropdownMenuLabel({ function DropdownMenuLabel({
@ -148,7 +148,7 @@ function DropdownMenuLabel({
inset, inset,
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & { }: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean inset?: boolean;
}) { }) {
return ( return (
<DropdownMenuPrimitive.Label <DropdownMenuPrimitive.Label
@ -156,11 +156,11 @@ function DropdownMenuLabel({
data-inset={inset} data-inset={inset}
className={cn( className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", "px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function DropdownMenuSeparator({ function DropdownMenuSeparator({
@ -173,7 +173,7 @@ function DropdownMenuSeparator({
className={cn("bg-border -mx-1 my-1 h-px", className)} className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props} {...props}
/> />
) );
} }
function DropdownMenuShortcut({ function DropdownMenuShortcut({
@ -185,17 +185,17 @@ function DropdownMenuShortcut({
data-slot="dropdown-menu-shortcut" data-slot="dropdown-menu-shortcut"
className={cn( className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest", "text-muted-foreground ml-auto text-xs tracking-widest",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function DropdownMenuSub({ function DropdownMenuSub({
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} /> return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
} }
function DropdownMenuSubTrigger({ function DropdownMenuSubTrigger({
@ -204,7 +204,7 @@ function DropdownMenuSubTrigger({
children, children,
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & { }: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean inset?: boolean;
}) { }) {
return ( return (
<DropdownMenuPrimitive.SubTrigger <DropdownMenuPrimitive.SubTrigger
@ -212,14 +212,14 @@ function DropdownMenuSubTrigger({
data-inset={inset} data-inset={inset}
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className,
)} )}
{...props} {...props}
> >
{children} {children}
<ChevronRightIcon className="ml-auto size-4" /> <ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger> </DropdownMenuPrimitive.SubTrigger>
) );
} }
function DropdownMenuSubContent({ function DropdownMenuSubContent({
@ -231,11 +231,11 @@ function DropdownMenuSubContent({
data-slot="dropdown-menu-sub-content" data-slot="dropdown-menu-sub-content"
className={cn( className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg", "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
export { export {
@ -254,4 +254,4 @@ export {
DropdownMenuSub, DropdownMenuSub,
DropdownMenuSubTrigger, DropdownMenuSubTrigger,
DropdownMenuSubContent, DropdownMenuSubContent,
} };

View File

@ -1,6 +1,6 @@
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Empty({ className, ...props }: React.ComponentProps<"div">) { function Empty({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
@ -8,11 +8,11 @@ function Empty({ className, ...props }: React.ComponentProps<"div">) {
data-slot="empty" data-slot="empty"
className={cn( className={cn(
"flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12", "flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) { function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
@ -21,11 +21,11 @@ function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
data-slot="empty-header" data-slot="empty-header"
className={cn( className={cn(
"flex max-w-sm flex-col items-center gap-2 text-center", "flex max-w-sm flex-col items-center gap-2 text-center",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
const emptyMediaVariants = cva( const emptyMediaVariants = cva(
@ -40,8 +40,8 @@ const emptyMediaVariants = cva(
defaultVariants: { defaultVariants: {
variant: "default", variant: "default",
}, },
} },
) );
function EmptyMedia({ function EmptyMedia({
className, className,
@ -55,7 +55,7 @@ function EmptyMedia({
className={cn(emptyMediaVariants({ variant, className }))} className={cn(emptyMediaVariants({ variant, className }))}
{...props} {...props}
/> />
) );
} }
function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) { function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
@ -65,7 +65,7 @@ function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
className={cn("text-lg font-medium tracking-tight", className)} className={cn("text-lg font-medium tracking-tight", className)}
{...props} {...props}
/> />
) );
} }
function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) { function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
@ -74,11 +74,11 @@ function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
data-slot="empty-description" data-slot="empty-description"
className={cn( className={cn(
"text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4", "text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function EmptyContent({ className, ...props }: React.ComponentProps<"div">) { function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
@ -87,11 +87,11 @@ function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
data-slot="empty-content" data-slot="empty-content"
className={cn( className={cn(
"flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance", "flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
export { export {
@ -101,4 +101,4 @@ export {
EmptyDescription, EmptyDescription,
EmptyContent, EmptyContent,
EmptyMedia, EmptyMedia,
} };

View File

@ -1,14 +1,14 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import * as HoverCardPrimitive from "@radix-ui/react-hover-card" import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function HoverCard({ function HoverCard({
...props ...props
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) { }: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} /> return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />;
} }
function HoverCardTrigger({ function HoverCardTrigger({
@ -16,7 +16,7 @@ function HoverCardTrigger({
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) { }: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
return ( return (
<HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} /> <HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
) );
} }
function HoverCardContent({ function HoverCardContent({
@ -33,12 +33,12 @@ function HoverCardContent({
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden", "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
className className,
)} )}
{...props} {...props}
/> />
</HoverCardPrimitive.Portal> </HoverCardPrimitive.Portal>
) );
} }
export { HoverCard, HoverCardTrigger, HoverCardContent } export { HoverCard, HoverCardTrigger, HoverCardContent };

View File

@ -1,9 +1,9 @@
import * as React from "react" import * as React from "react";
import { Slot } from "@radix-ui/react-slot" import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
import { Separator } from "@/components/ui/separator" import { Separator } from "@/components/ui/separator";
function ItemGroup({ className, ...props }: React.ComponentProps<"div">) { function ItemGroup({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
@ -13,7 +13,7 @@ function ItemGroup({ className, ...props }: React.ComponentProps<"div">) {
className={cn("group/item-group flex flex-col", className)} className={cn("group/item-group flex flex-col", className)}
{...props} {...props}
/> />
) );
} }
function ItemSeparator({ function ItemSeparator({
@ -27,7 +27,7 @@ function ItemSeparator({
className={cn("my-0", className)} className={cn("my-0", className)}
{...props} {...props}
/> />
) );
} }
const itemVariants = cva( const itemVariants = cva(
@ -48,8 +48,8 @@ const itemVariants = cva(
variant: "default", variant: "default",
size: "default", size: "default",
}, },
} },
) );
function Item({ function Item({
className, className,
@ -59,7 +59,7 @@ function Item({
...props ...props
}: React.ComponentProps<"div"> & }: React.ComponentProps<"div"> &
VariantProps<typeof itemVariants> & { asChild?: boolean }) { VariantProps<typeof itemVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "div" const Comp = asChild ? Slot : "div";
return ( return (
<Comp <Comp
data-slot="item" data-slot="item"
@ -68,7 +68,7 @@ function Item({
className={cn(itemVariants({ variant, size, className }))} className={cn(itemVariants({ variant, size, className }))}
{...props} {...props}
/> />
) );
} }
const itemMediaVariants = cva( const itemMediaVariants = cva(
@ -85,8 +85,8 @@ const itemMediaVariants = cva(
defaultVariants: { defaultVariants: {
variant: "default", variant: "default",
}, },
} },
) );
function ItemMedia({ function ItemMedia({
className, className,
@ -100,7 +100,7 @@ function ItemMedia({
className={cn(itemMediaVariants({ variant, className }))} className={cn(itemMediaVariants({ variant, className }))}
{...props} {...props}
/> />
) );
} }
function ItemContent({ className, ...props }: React.ComponentProps<"div">) { function ItemContent({ className, ...props }: React.ComponentProps<"div">) {
@ -109,11 +109,11 @@ function ItemContent({ className, ...props }: React.ComponentProps<"div">) {
data-slot="item-content" data-slot="item-content"
className={cn( className={cn(
"flex flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none", "flex flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function ItemTitle({ className, ...props }: React.ComponentProps<"div">) { function ItemTitle({ className, ...props }: React.ComponentProps<"div">) {
@ -122,11 +122,11 @@ function ItemTitle({ className, ...props }: React.ComponentProps<"div">) {
data-slot="item-title" data-slot="item-title"
className={cn( className={cn(
"flex w-fit items-center gap-2 text-sm leading-snug font-medium", "flex w-fit items-center gap-2 text-sm leading-snug font-medium",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function ItemDescription({ className, ...props }: React.ComponentProps<"p">) { function ItemDescription({ className, ...props }: React.ComponentProps<"p">) {
@ -136,11 +136,11 @@ function ItemDescription({ className, ...props }: React.ComponentProps<"p">) {
className={cn( className={cn(
"text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance", "text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance",
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4", "[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function ItemActions({ className, ...props }: React.ComponentProps<"div">) { function ItemActions({ className, ...props }: React.ComponentProps<"div">) {
@ -150,7 +150,7 @@ function ItemActions({ className, ...props }: React.ComponentProps<"div">) {
className={cn("flex items-center gap-2", className)} className={cn("flex items-center gap-2", className)}
{...props} {...props}
/> />
) );
} }
function ItemHeader({ className, ...props }: React.ComponentProps<"div">) { function ItemHeader({ className, ...props }: React.ComponentProps<"div">) {
@ -159,11 +159,11 @@ function ItemHeader({ className, ...props }: React.ComponentProps<"div">) {
data-slot="item-header" data-slot="item-header"
className={cn( className={cn(
"flex basis-full items-center justify-between gap-2", "flex basis-full items-center justify-between gap-2",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function ItemFooter({ className, ...props }: React.ComponentProps<"div">) { function ItemFooter({ className, ...props }: React.ComponentProps<"div">) {
@ -172,11 +172,11 @@ function ItemFooter({ className, ...props }: React.ComponentProps<"div">) {
data-slot="item-footer" data-slot="item-footer"
className={cn( className={cn(
"flex basis-full items-center justify-between gap-2", "flex basis-full items-center justify-between gap-2",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
export { export {
@ -190,4 +190,4 @@ export {
ItemDescription, ItemDescription,
ItemHeader, ItemHeader,
ItemFooter, ItemFooter,
} };

View File

@ -145,7 +145,7 @@
/* Border glow effect */ /* Border glow effect */
.magic-bento-card--border-glow::after { .magic-bento-card--border-glow::after {
content: ''; content: "";
position: absolute; position: absolute;
inset: 0; inset: 0;
padding: 6px; padding: 6px;
@ -186,7 +186,7 @@
} }
.particle::before { .particle::before {
content: ''; content: "";
position: absolute; position: absolute;
top: -2px; top: -2px;
left: -2px; left: -2px;

View File

@ -1,9 +1,9 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import * as ProgressPrimitive from "@radix-ui/react-progress" import * as ProgressPrimitive from "@radix-ui/react-progress";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Progress({ function Progress({
className, className,
@ -15,7 +15,7 @@ function Progress({
data-slot="progress" data-slot="progress"
className={cn( className={cn(
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full", "bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
className className,
)} )}
{...props} {...props}
> >
@ -25,7 +25,7 @@ function Progress({
style={{ transform: `translateX(-${100 - (value || 0)}%)` }} style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/> />
</ProgressPrimitive.Root> </ProgressPrimitive.Root>
) );
} }
export { Progress } export { Progress };

View File

@ -1,9 +1,9 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function ScrollArea({ function ScrollArea({
className, className,
@ -25,7 +25,7 @@ function ScrollArea({
<ScrollBar /> <ScrollBar />
<ScrollAreaPrimitive.Corner /> <ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root> </ScrollAreaPrimitive.Root>
) );
} }
function ScrollBar({ function ScrollBar({
@ -43,7 +43,7 @@ function ScrollBar({
"h-full w-2.5 border-l border-l-transparent", "h-full w-2.5 border-l border-l-transparent",
orientation === "horizontal" && orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent", "h-2.5 flex-col border-t border-t-transparent",
className className,
)} )}
{...props} {...props}
> >
@ -52,7 +52,7 @@ function ScrollBar({
className="bg-border relative flex-1 rounded-full" className="bg-border relative flex-1 rounded-full"
/> />
</ScrollAreaPrimitive.ScrollAreaScrollbar> </ScrollAreaPrimitive.ScrollAreaScrollbar>
) );
} }
export { ScrollArea, ScrollBar } export { ScrollArea, ScrollBar };

View File

@ -1,9 +1,9 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import * as SeparatorPrimitive from "@radix-ui/react-separator" import * as SeparatorPrimitive from "@radix-ui/react-separator";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Separator({ function Separator({
className, className,
@ -18,11 +18,11 @@ function Separator({
orientation={orientation} orientation={orientation}
className={cn( className={cn(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px", "bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
export { Separator } export { Separator };

View File

@ -1,31 +1,31 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import * as SheetPrimitive from "@radix-ui/react-dialog" import * as SheetPrimitive from "@radix-ui/react-dialog";
import { XIcon } from "lucide-react" import { XIcon } from "lucide-react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) { function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
return <SheetPrimitive.Root data-slot="sheet" {...props} /> return <SheetPrimitive.Root data-slot="sheet" {...props} />;
} }
function SheetTrigger({ function SheetTrigger({
...props ...props
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) { }: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} /> return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
} }
function SheetClose({ function SheetClose({
...props ...props
}: React.ComponentProps<typeof SheetPrimitive.Close>) { }: React.ComponentProps<typeof SheetPrimitive.Close>) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} /> return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
} }
function SheetPortal({ function SheetPortal({
...props ...props
}: React.ComponentProps<typeof SheetPrimitive.Portal>) { }: React.ComponentProps<typeof SheetPrimitive.Portal>) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} /> return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
} }
function SheetOverlay({ function SheetOverlay({
@ -37,11 +37,11 @@ function SheetOverlay({
data-slot="sheet-overlay" data-slot="sheet-overlay"
className={cn( className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50", "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function SheetContent({ function SheetContent({
@ -50,7 +50,7 @@ function SheetContent({
side = "right", side = "right",
...props ...props
}: React.ComponentProps<typeof SheetPrimitive.Content> & { }: React.ComponentProps<typeof SheetPrimitive.Content> & {
side?: "top" | "right" | "bottom" | "left" side?: "top" | "right" | "bottom" | "left";
}) { }) {
return ( return (
<SheetPortal> <SheetPortal>
@ -67,7 +67,7 @@ function SheetContent({
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b", "data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
side === "bottom" && side === "bottom" &&
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t", "data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
className className,
)} )}
{...props} {...props}
> >
@ -78,7 +78,7 @@ function SheetContent({
</SheetPrimitive.Close> </SheetPrimitive.Close>
</SheetPrimitive.Content> </SheetPrimitive.Content>
</SheetPortal> </SheetPortal>
) );
} }
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
@ -88,7 +88,7 @@ function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
className={cn("flex flex-col gap-1.5 p-4", className)} className={cn("flex flex-col gap-1.5 p-4", className)}
{...props} {...props}
/> />
) );
} }
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
@ -98,7 +98,7 @@ function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
className={cn("mt-auto flex flex-col gap-2 p-4", className)} className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props} {...props}
/> />
) );
} }
function SheetTitle({ function SheetTitle({
@ -111,7 +111,7 @@ function SheetTitle({
className={cn("text-foreground font-semibold", className)} className={cn("text-foreground font-semibold", className)}
{...props} {...props}
/> />
) );
} }
function SheetDescription({ function SheetDescription({
@ -124,7 +124,7 @@ function SheetDescription({
className={cn("text-muted-foreground text-sm", className)} className={cn("text-muted-foreground text-sm", className)}
{...props} {...props}
/> />
) );
} }
export { export {
@ -136,4 +136,4 @@ export {
SheetFooter, SheetFooter,
SheetTitle, SheetTitle,
SheetDescription, SheetDescription,
} };

View File

@ -1,25 +1,25 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
interface ShineBorderProps extends React.HTMLAttributes<HTMLDivElement> { interface ShineBorderProps extends React.HTMLAttributes<HTMLDivElement> {
/** /**
* Width of the border in pixels * Width of the border in pixels
* @default 1 * @default 1
*/ */
borderWidth?: number borderWidth?: number;
/** /**
* Duration of the animation in seconds * Duration of the animation in seconds
* @default 14 * @default 14
*/ */
duration?: number duration?: number;
/** /**
* Color of the border, can be a single color or an array of colors * Color of the border, can be a single color or an array of colors
* @default "#000000" * @default "#000000"
*/ */
shineColor?: string | string[] shineColor?: string | string[];
} }
/** /**
@ -55,9 +55,9 @@ export function ShineBorder({
} }
className={cn( className={cn(
"motion-safe:animate-shine pointer-events-none absolute inset-0 size-full rounded-[inherit] will-change-[background-position]", "motion-safe:animate-shine pointer-events-none absolute inset-0 size-full rounded-[inherit] will-change-[background-position]",
className className,
)} )}
{...props} {...props}
/> />
) );
} }

View File

@ -1,4 +1,4 @@
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Skeleton({ className, ...props }: React.ComponentProps<"div">) { function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
@ -7,7 +7,7 @@ function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
className={cn("bg-accent animate-pulse rounded-md", className)} className={cn("bg-accent animate-pulse rounded-md", className)}
{...props} {...props}
/> />
) );
} }
export { Skeleton } export { Skeleton };

View File

@ -1,4 +1,4 @@
"use client" "use client";
import { import {
CircleCheckIcon, CircleCheckIcon,
@ -6,12 +6,12 @@ import {
Loader2Icon, Loader2Icon,
OctagonXIcon, OctagonXIcon,
TriangleAlertIcon, TriangleAlertIcon,
} from "lucide-react" } from "lucide-react";
import { useTheme } from "next-themes" import { useTheme } from "next-themes";
import { Toaster as Sonner, type ToasterProps } from "sonner" import { Toaster as Sonner, type ToasterProps } from "sonner";
const Toaster = ({ ...props }: ToasterProps) => { const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme() const { theme = "system" } = useTheme();
return ( return (
<Sonner <Sonner
@ -34,7 +34,7 @@ const Toaster = ({ ...props }: ToasterProps) => {
} }
{...props} {...props}
/> />
) );
} };
export { Toaster } export { Toaster };

View File

@ -11,13 +11,17 @@
} }
.card-spotlight::before { .card-spotlight::before {
content: ''; content: "";
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
background: radial-gradient(circle at var(--mouse-x) var(--mouse-y), var(--spotlight-color), transparent 80%); background: radial-gradient(
circle at var(--mouse-x) var(--mouse-y),
var(--spotlight-color),
transparent 80%
);
opacity: 0; opacity: 0;
transition: opacity 0.5s ease; transition: opacity 0.5s ease;
pointer-events: none; pointer-events: none;

View File

@ -1,9 +1,9 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import * as SwitchPrimitive from "@radix-ui/react-switch" import * as SwitchPrimitive from "@radix-ui/react-switch";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Switch({ function Switch({
className, className,
@ -14,18 +14,18 @@ function Switch({
data-slot="switch" data-slot="switch"
className={cn( className={cn(
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50", "peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className className,
)} )}
{...props} {...props}
> >
<SwitchPrimitive.Thumb <SwitchPrimitive.Thumb
data-slot="switch-thumb" data-slot="switch-thumb"
className={cn( className={cn(
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0" "bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0",
)} )}
/> />
</SwitchPrimitive.Root> </SwitchPrimitive.Root>
) );
} }
export { Switch } export { Switch };

View File

@ -1,10 +1,10 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs" import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Tabs({ function Tabs({
className, className,
@ -18,11 +18,11 @@ function Tabs({
orientation={orientation} orientation={orientation}
className={cn( className={cn(
"group/tabs flex gap-2 data-[orientation=horizontal]:flex-col", "group/tabs flex gap-2 data-[orientation=horizontal]:flex-col",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
const tabsListVariants = cva( const tabsListVariants = cva(
@ -37,8 +37,8 @@ const tabsListVariants = cva(
defaultVariants: { defaultVariants: {
variant: "default", variant: "default",
}, },
} },
) );
function TabsList({ function TabsList({
className, className,
@ -53,7 +53,7 @@ function TabsList({
className={cn(tabsListVariants({ variant }), className)} className={cn(tabsListVariants({ variant }), className)}
{...props} {...props}
/> />
) );
} }
function TabsTrigger({ function TabsTrigger({
@ -68,11 +68,11 @@ function TabsTrigger({
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent", "group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent",
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 data-[state=active]:text-foreground", "data-[state=active]:bg-background dark:data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 data-[state=active]:text-foreground",
"after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100", "after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function TabsContent({ function TabsContent({
@ -85,7 +85,7 @@ function TabsContent({
className={cn("flex-1 outline-none", className)} className={cn("flex-1 outline-none", className)}
{...props} {...props}
/> />
) );
} }
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants } export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants };

View File

@ -1,10 +1,10 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import * as TogglePrimitive from "@radix-ui/react-toggle" import * as TogglePrimitive from "@radix-ui/react-toggle";
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const toggleVariants = cva( const toggleVariants = cva(
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap", "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
@ -25,8 +25,8 @@ const toggleVariants = cva(
variant: "default", variant: "default",
size: "default", size: "default",
}, },
} },
) );
function Toggle({ function Toggle({
className, className,
@ -41,7 +41,7 @@ function Toggle({
className={cn(toggleVariants({ variant, size, className }))} className={cn(toggleVariants({ variant, size, className }))}
{...props} {...props}
/> />
) );
} }
export { Toggle, toggleVariants } export { Toggle, toggleVariants };

View File

@ -23,7 +23,7 @@ export function ArtifactLink(props: AnchorHTMLAttributes<HTMLAnchorElement>) {
<a <a
{...rest} {...rest}
className={cn( className={cn(
"text-primary underline decoration-primary/30 underline-offset-2 hover:decoration-primary/60 transition-colors", "text-primary decoration-primary/30 hover:decoration-primary/60 underline underline-offset-2 transition-colors",
className, className,
)} )}
target={target ?? (external ? "_blank" : undefined)} target={target ?? (external ? "_blank" : undefined)}

View File

@ -9,13 +9,13 @@ import {
} from "@/components/ui/hover-card"; } from "@/components/ui/hover-card";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
export function CitationLink({ export function CitationLink({
href, href,
children, children,
...props ...props
}: ComponentProps<"a">) { }: ComponentProps<"a">) {
const domain = extractDomain(href ?? ""); const domain = extractDomain(href ?? "");
// Priority: children > domain // Priority: children > domain
const childrenText = const childrenText =
typeof children === "string" typeof children === "string"
@ -48,12 +48,12 @@ export function CitationLink({
<div className="p-3"> <div className="p-3">
<div className="space-y-1"> <div className="space-y-1">
{displayText && ( {displayText && (
<h4 className="truncate font-medium text-sm leading-tight"> <h4 className="truncate text-sm leading-tight font-medium">
{displayText} {displayText}
</h4> </h4>
)} )}
{href && ( {href && (
<p className="truncate break-all text-muted-foreground text-xs"> <p className="text-muted-foreground truncate text-xs break-all">
{href} {href}
</p> </p>
)} )}

View File

@ -63,7 +63,6 @@ export function CommandPalette() {
useGlobalShortcuts(shortcuts); useGlobalShortcuts(shortcuts);
const isMac = const isMac =
typeof navigator !== "undefined" && navigator.userAgent.includes("Mac"); typeof navigator !== "undefined" && navigator.userAgent.includes("Mac");
const metaKey = isMac ? "⌘" : "Ctrl+"; const metaKey = isMac ? "⌘" : "Ctrl+";
@ -80,7 +79,10 @@ export function CommandPalette() {
<CommandItem onSelect={handleNewChat}> <CommandItem onSelect={handleNewChat}>
<MessageSquarePlusIcon className="mr-2 h-4 w-4" /> <MessageSquarePlusIcon className="mr-2 h-4 w-4" />
{t.sidebar.newChat} {t.sidebar.newChat}
<CommandShortcut>{metaKey}{shiftKey}N</CommandShortcut> <CommandShortcut>
{metaKey}
{shiftKey}N
</CommandShortcut>
</CommandItem> </CommandItem>
<CommandItem onSelect={handleOpenSettings}> <CommandItem onSelect={handleOpenSettings}>
<SettingsIcon className="mr-2 h-4 w-4" /> <SettingsIcon className="mr-2 h-4 w-4" />

View File

@ -48,7 +48,10 @@ export function MarkdownContent({
return ( return (
<a <a
{...rest} {...rest}
className={cn("text-primary underline decoration-primary/30 underline-offset-2 hover:decoration-primary/60 transition-colors", className)} className={cn(
"text-primary decoration-primary/30 hover:decoration-primary/60 underline underline-offset-2 transition-colors",
className,
)}
target={target ?? (external ? "_blank" : undefined)} target={target ?? (external ? "_blank" : undefined)}
rel={rel ?? (external ? "noopener noreferrer" : undefined)} rel={rel ?? (external ? "noopener noreferrer" : undefined)}
/> />

View File

@ -29,7 +29,10 @@ function getModeDescriptionKey(
mode: AgentMode, mode: AgentMode,
): keyof Pick< ): keyof Pick<
Translations["inputBox"], Translations["inputBox"],
"flashModeDescription" | "reasoningModeDescription" | "proModeDescription" | "ultraModeDescription" | "flashModeDescription"
| "reasoningModeDescription"
| "proModeDescription"
| "ultraModeDescription"
> { > {
switch (mode) { switch (mode) {
case "flash": case "flash":

View File

@ -33,17 +33,20 @@ DeerFlow is proudly open source and distributed under the **MIT License**.
We extend our heartfelt gratitude to the open source projects and contributors who have made DeerFlow a reality. We truly stand on the shoulders of giants. We extend our heartfelt gratitude to the open source projects and contributors who have made DeerFlow a reality. We truly stand on the shoulders of giants.
### Core Frameworks ### Core Frameworks
- **[LangChain](https://github.com/langchain-ai/langchain)**: A phenomenal framework that powers our LLM interactions and chains. - **[LangChain](https://github.com/langchain-ai/langchain)**: A phenomenal framework that powers our LLM interactions and chains.
- **[LangGraph](https://github.com/langchain-ai/langgraph)**: Enabling sophisticated multi-agent orchestration. - **[LangGraph](https://github.com/langchain-ai/langgraph)**: Enabling sophisticated multi-agent orchestration.
- **[Next.js](https://nextjs.org/)**: A cutting-edge framework for building web applications. - **[Next.js](https://nextjs.org/)**: A cutting-edge framework for building web applications.
### UI Libraries ### UI Libraries
- **[Shadcn](https://ui.shadcn.com/)**: Minimalistic components that power our UI. - **[Shadcn](https://ui.shadcn.com/)**: Minimalistic components that power our UI.
- **[SToneX](https://github.com/stonexer)**: For his invaluable contribution to token-by-token visual effects. - **[SToneX](https://github.com/stonexer)**: For his invaluable contribution to token-by-token visual effects.
These outstanding projects form the backbone of DeerFlow and exemplify the transformative power of open source collaboration. These outstanding projects form the backbone of DeerFlow and exemplify the transformative power of open source collaboration.
### Special Thanks ### Special Thanks
Finally, we want to express our heartfelt gratitude to the core authors of DeerFlow 1.0 and 2.0: Finally, we want to express our heartfelt gratitude to the core authors of DeerFlow 1.0 and 2.0:
- **[Daniel Walnut](https://github.com/hetaoBackend/)** - **[Daniel Walnut](https://github.com/hetaoBackend/)**

View File

@ -227,8 +227,7 @@ export function MemorySettingsPage() {
const filterAll = t.settings.memory.filterAll ?? "All"; const filterAll = t.settings.memory.filterAll ?? "All";
const filterFacts = t.settings.memory.filterFacts ?? "Facts"; const filterFacts = t.settings.memory.filterFacts ?? "Facts";
const filterSummaries = t.settings.memory.filterSummaries ?? "Summaries"; const filterSummaries = t.settings.memory.filterSummaries ?? "Summaries";
const noMatches = const noMatches = t.settings.memory.noMatches ?? "No matching memory found";
t.settings.memory.noMatches ?? "No matching memory found";
const sectionGroups = memory ? buildMemorySectionGroups(memory, t) : []; const sectionGroups = memory ? buildMemorySectionGroups(memory, t) : [];
const filteredSectionGroups = sectionGroups const filteredSectionGroups = sectionGroups
@ -295,7 +294,9 @@ export function MemorySettingsPage() {
description={t.settings.memory.description} description={t.settings.memory.description}
> >
{isLoading ? ( {isLoading ? (
<div className="text-muted-foreground text-sm">{t.common.loading}</div> <div className="text-muted-foreground text-sm">
{t.common.loading}
</div>
) : error ? ( ) : error ? (
<div>Error: {error.message}</div> <div>Error: {error.message}</div>
) : !memory ? ( ) : !memory ? (
@ -408,7 +409,9 @@ export function MemorySettingsPage() {
{formatTimeAgo(fact.createdAt)} {formatTimeAgo(fact.createdAt)}
</span> </span>
</div> </div>
<p className="break-words text-sm">{fact.content}</p> <p className="text-sm break-words">
{fact.content}
</p>
<Link <Link
href={pathOfThread(fact.source)} href={pathOfThread(fact.source)}
className="text-primary text-sm underline-offset-4 hover:underline" className="text-primary text-sm underline-offset-4 hover:underline"
@ -443,9 +446,7 @@ export function MemorySettingsPage() {
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>{clearAllConfirmTitle}</DialogTitle> <DialogTitle>{clearAllConfirmTitle}</DialogTitle>
<DialogDescription> <DialogDescription>{clearAllConfirmDescription}</DialogDescription>
{clearAllConfirmDescription}
</DialogDescription>
</DialogHeader> </DialogHeader>
<DialogFooter> <DialogFooter>
<Button <Button

View File

@ -9,10 +9,7 @@ function getBaseOrigin() {
export function getBackendBaseURL() { export function getBackendBaseURL() {
if (env.NEXT_PUBLIC_BACKEND_BASE_URL) { if (env.NEXT_PUBLIC_BACKEND_BASE_URL) {
return new URL( return new URL(env.NEXT_PUBLIC_BACKEND_BASE_URL, getBaseOrigin())
env.NEXT_PUBLIC_BACKEND_BASE_URL,
getBaseOrigin(),
)
.toString() .toString()
.replace(/\/+$/, ""); .replace(/\/+$/, "");
} else { } else {

View File

@ -281,14 +281,15 @@ export const enUS: Translations = {
output: "Output", output: "Output",
total: "Total", total: "Total",
}, },
// Shortcuts // Shortcuts
shortcuts: { shortcuts: {
searchActions: "Search actions...", searchActions: "Search actions...",
noResults: "No results found.", noResults: "No results found.",
actions: "Actions", actions: "Actions",
keyboardShortcuts: "Keyboard Shortcuts", keyboardShortcuts: "Keyboard Shortcuts",
keyboardShortcutsDescription: "Navigate DeerFlow faster with keyboard shortcuts.", keyboardShortcutsDescription:
"Navigate DeerFlow faster with keyboard shortcuts.",
openCommandPalette: "Open Command Palette", openCommandPalette: "Open Command Palette",
toggleSidebar: "Toggle Sidebar", toggleSidebar: "Toggle Sidebar",
}, },

View File

@ -218,7 +218,7 @@ export interface Translations {
output: string; output: string;
total: string; total: string;
}; };
// Shortcuts // Shortcuts
shortcuts: { shortcuts: {
searchActions: string; searchActions: string;
@ -254,16 +254,16 @@ export interface Translations {
factDeleteConfirmTitle: string; factDeleteConfirmTitle: string;
factDeleteConfirmDescription: string; factDeleteConfirmDescription: string;
factDeleteSuccess: string; factDeleteSuccess: string;
noFacts: string; noFacts: string;
summaryReadOnly: string; summaryReadOnly: string;
memoryFullyEmpty: string; memoryFullyEmpty: string;
factPreviewLabel: string; factPreviewLabel: string;
searchPlaceholder: string; searchPlaceholder: string;
filterAll: string; filterAll: string;
filterFacts: string; filterFacts: string;
filterSummaries: string; filterSummaries: string;
noMatches: string; noMatches: string;
markdown: { markdown: {
overview: string; overview: string;
userContext: string; userContext: string;
work: string; work: string;

View File

@ -268,7 +268,7 @@ export const zhCN: Translations = {
output: "输出", output: "输出",
total: "总计", total: "总计",
}, },
// Shortcuts // Shortcuts
shortcuts: { shortcuts: {
searchActions: "搜索操作...", searchActions: "搜索操作...",
@ -308,7 +308,8 @@ export const zhCN: Translations = {
"这条事实会立即从记忆中删除。此操作无法撤销。", "这条事实会立即从记忆中删除。此操作无法撤销。",
factDeleteSuccess: "事实已删除", factDeleteSuccess: "事实已删除",
noFacts: "还没有保存的事实。", noFacts: "还没有保存的事实。",
summaryReadOnly: "摘要分区当前仍为只读。现在你可以清空全部记忆或删除单条事实。", summaryReadOnly:
"摘要分区当前仍为只读。现在你可以清空全部记忆或删除单条事实。",
memoryFullyEmpty: "还没有保存任何记忆。", memoryFullyEmpty: "还没有保存任何记忆。",
factPreviewLabel: "即将删除的事实", factPreviewLabel: "即将删除的事实",
searchPlaceholder: "搜索记忆", searchPlaceholder: "搜索记忆",

View File

@ -8,14 +8,12 @@ export async function loadMCPConfig() {
} }
export async function updateMCPConfig(config: MCPConfig) { export async function updateMCPConfig(config: MCPConfig) {
const response = await fetch(`${getBackendBaseURL()}/api/mcp/config`, const response = await fetch(`${getBackendBaseURL()}/api/mcp/config`, {
{ method: "PUT",
method: "PUT", headers: {
headers: { "Content-Type": "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify(config),
}, },
); body: JSON.stringify(config),
});
return response.json(); return response.json();
} }

View File

@ -10,7 +10,9 @@ async function readMemoryResponse(
const errorData = (await response.json().catch(() => ({}))) as { const errorData = (await response.json().catch(() => ({}))) as {
detail?: string; detail?: string;
}; };
throw new Error(errorData.detail ?? `${fallbackMessage}: ${response.statusText}`); throw new Error(
errorData.detail ?? `${fallbackMessage}: ${response.statusText}`,
);
} }
return response.json() as Promise<UserMemory>; return response.json() as Promise<UserMemory>;

View File

@ -10,9 +10,7 @@ export interface TokenUsage {
* Extract usage_metadata from an AI message if present. * Extract usage_metadata from an AI message if present.
* The field is added by the backend (PR #1218) but not typed in the SDK. * The field is added by the backend (PR #1218) but not typed in the SDK.
*/ */
function getUsageMetadata( function getUsageMetadata(message: Message): TokenUsage | null {
message: Message,
): TokenUsage | null {
if (message.type !== "ai") { if (message.type !== "ai") {
return null; return null;
} }

View File

@ -127,7 +127,10 @@ export function groupMessages<T>(
export function extractTextFromMessage(message: Message) { export function extractTextFromMessage(message: Message) {
if (typeof message.content === "string") { if (typeof message.content === "string") {
return splitInlineReasoningFromAIMessage(message)?.content ?? message.content.trim(); return (
splitInlineReasoningFromAIMessage(message)?.content ??
message.content.trim()
);
} }
if (Array.isArray(message.content)) { if (Array.isArray(message.content)) {
return message.content return message.content
@ -167,7 +170,10 @@ function splitInlineReasoningFromAIMessage(message: Message) {
export function extractContentFromMessage(message: Message) { export function extractContentFromMessage(message: Message) {
if (typeof message.content === "string") { if (typeof message.content === "string") {
return splitInlineReasoningFromAIMessage(message)?.content ?? message.content.trim(); return (
splitInlineReasoningFromAIMessage(message)?.content ??
message.content.trim()
);
} }
if (Array.isArray(message.content)) { if (Array.isArray(message.content)) {
return message.content return message.content
@ -233,8 +239,11 @@ export function extractURLFromImageURLContent(
export function hasContent(message: Message) { export function hasContent(message: Message) {
if (typeof message.content === "string") { if (typeof message.content === "string") {
return ( return (
splitInlineReasoningFromAIMessage(message)?.content ?? message.content.trim() (
).length > 0; splitInlineReasoningFromAIMessage(message)?.content ??
message.content.trim()
).length > 0
);
} }
if (Array.isArray(message.content)) { if (Array.isArray(message.content)) {
return message.content.length > 0; return message.content.length > 0;

Some files were not shown because too many files have changed in this diff Show More