mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-04-25 11:18:22 +00:00
ci: enforce code formatting checks for backend and frontend (#1536)
This commit is contained in:
parent
06a623f9c8
commit
084dc7e748
5
.github/workflows/lint-check.yml
vendored
5
.github/workflows/lint-check.yml
vendored
@ -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
|
||||||
|
|||||||
@ -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 .
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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,
|
"device_id": device_id,
|
||||||
"account_uuid": "deerflow",
|
"account_uuid": "deerflow",
|
||||||
"session_id": session_id,
|
"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."""
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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())
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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"
|
||||||
@ -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
1
frontend/.prettierignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
pnpm-lock.yaml
|
||||||
@ -11,7 +11,7 @@ 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) |
|
||||||
@ -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
|
||||||
|
|||||||
@ -115,7 +115,7 @@ 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 |
|
||||||
|
|||||||
@ -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_
|
||||||
|
|||||||
@ -30,9 +30,9 @@
|
|||||||
--color-info: #3b82f6;
|
--color-info: #3b82f6;
|
||||||
|
|
||||||
/* 字体 */
|
/* 字体 */
|
||||||
--font-heading: 'Oswald', sans-serif;
|
--font-heading: "Oswald", sans-serif;
|
||||||
--font-body: 'Inter', sans-serif;
|
--font-body: "Inter", sans-serif;
|
||||||
--font-display: 'Montserrat', sans-serif;
|
--font-display: "Montserrat", sans-serif;
|
||||||
|
|
||||||
/* 尺寸 */
|
/* 尺寸 */
|
||||||
--container-max: 1280px;
|
--container-max: 1280px;
|
||||||
@ -44,9 +44,12 @@
|
|||||||
|
|
||||||
/* 阴影 */
|
/* 阴影 */
|
||||||
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
--shadow-md:
|
||||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
--shadow-lg:
|
||||||
|
0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||||
|
--shadow-xl:
|
||||||
|
0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||||
--shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
--shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||||
|
|
||||||
/* 过渡 */
|
/* 过渡 */
|
||||||
@ -95,7 +98,9 @@ body {
|
|||||||
color: var(--color-gray-800);
|
color: var(--color-gray-800);
|
||||||
background-color: var(--color-white);
|
background-color: var(--color-white);
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
transition: background-color var(--transition-normal), color var(--transition-normal);
|
transition:
|
||||||
|
background-color var(--transition-normal),
|
||||||
|
color var(--transition-normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
@ -112,14 +117,20 @@ body {
|
|||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-dark) 100%);
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
var(--color-primary) 0%,
|
||||||
|
var(--color-primary-dark) 100%
|
||||||
|
);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
transition: opacity var(--transition-normal), visibility var(--transition-normal);
|
transition:
|
||||||
|
opacity var(--transition-normal),
|
||||||
|
visibility var(--transition-normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
.loader.loaded {
|
.loader.loaded {
|
||||||
@ -134,7 +145,15 @@ body {
|
|||||||
.football {
|
.football {
|
||||||
width: 80px;
|
width: 80px;
|
||||||
height: 80px;
|
height: 80px;
|
||||||
background: linear-gradient(45deg, var(--color-white) 25%, var(--color-gray-200) 25%, var(--color-gray-200) 50%, var(--color-white) 50%, var(--color-white) 75%, var(--color-gray-200) 75%);
|
background: linear-gradient(
|
||||||
|
45deg,
|
||||||
|
var(--color-white) 25%,
|
||||||
|
var(--color-gray-200) 25%,
|
||||||
|
var(--color-gray-200) 50%,
|
||||||
|
var(--color-white) 50%,
|
||||||
|
var(--color-white) 75%,
|
||||||
|
var(--color-gray-200) 75%
|
||||||
|
);
|
||||||
background-size: 20px 20px;
|
background-size: 20px 20px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
margin: 0 auto 2rem;
|
margin: 0 auto 2rem;
|
||||||
@ -143,7 +162,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.football::before {
|
.football::before {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@ -205,14 +224,18 @@ body {
|
|||||||
.logo-ball {
|
.logo-ball {
|
||||||
width: 36px;
|
width: 36px;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-secondary) 100%);
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
var(--color-primary) 0%,
|
||||||
|
var(--color-secondary) 100%
|
||||||
|
);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
position: relative;
|
position: relative;
|
||||||
animation: var(--animation-pulse);
|
animation: var(--animation-pulse);
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-ball::before {
|
.logo-ball::before {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@ -268,7 +291,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.nav-link::after {
|
.nav-link::after {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
@ -362,7 +385,11 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-light) 100%);
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
var(--color-primary) 0%,
|
||||||
|
var(--color-primary-light) 100%
|
||||||
|
);
|
||||||
color: var(--color-white);
|
color: var(--color-white);
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
}
|
}
|
||||||
@ -373,7 +400,11 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary {
|
.btn-secondary {
|
||||||
background: linear-gradient(135deg, var(--color-secondary) 0%, var(--color-secondary-light) 100%);
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
var(--color-secondary) 0%,
|
||||||
|
var(--color-secondary-light) 100%
|
||||||
|
);
|
||||||
color: var(--color-white);
|
color: var(--color-white);
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
}
|
}
|
||||||
@ -423,10 +454,12 @@ body {
|
|||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: linear-gradient(135deg,
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
rgba(26, 86, 219, 0.1) 0%,
|
rgba(26, 86, 219, 0.1) 0%,
|
||||||
rgba(59, 130, 246, 0.05) 50%,
|
rgba(59, 130, 246, 0.05) 50%,
|
||||||
rgba(245, 158, 11, 0.1) 100%);
|
rgba(245, 158, 11, 0.1) 100%
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-pattern {
|
.hero-pattern {
|
||||||
@ -436,8 +469,16 @@ body {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-image:
|
background-image:
|
||||||
radial-gradient(circle at 25% 25%, rgba(26, 86, 219, 0.1) 2px, transparent 2px),
|
radial-gradient(
|
||||||
radial-gradient(circle at 75% 75%, rgba(245, 158, 11, 0.1) 2px, transparent 2px);
|
circle at 25% 25%,
|
||||||
|
rgba(26, 86, 219, 0.1) 2px,
|
||||||
|
transparent 2px
|
||||||
|
),
|
||||||
|
radial-gradient(
|
||||||
|
circle at 75% 75%,
|
||||||
|
rgba(245, 158, 11, 0.1) 2px,
|
||||||
|
transparent 2px
|
||||||
|
);
|
||||||
background-size: 60px 60px;
|
background-size: 60px 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -448,10 +489,12 @@ body {
|
|||||||
top: 50%;
|
top: 50%;
|
||||||
right: 10%;
|
right: 10%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
background: radial-gradient(circle at 30% 30%,
|
background: radial-gradient(
|
||||||
|
circle at 30% 30%,
|
||||||
rgba(26, 86, 219, 0.2) 0%,
|
rgba(26, 86, 219, 0.2) 0%,
|
||||||
rgba(26, 86, 219, 0.1) 30%,
|
rgba(26, 86, 219, 0.1) 30%,
|
||||||
transparent 70%);
|
transparent 70%
|
||||||
|
);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
animation: float 6s ease-in-out infinite;
|
animation: float 6s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
@ -515,7 +558,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.highlight::after {
|
.highlight::after {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
@ -581,7 +624,11 @@ body {
|
|||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: linear-gradient(135deg, var(--color-gray-100) 0%, var(--color-gray-200) 100%);
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
var(--color-gray-100) 0%,
|
||||||
|
var(--color-gray-200) 100%
|
||||||
|
);
|
||||||
border-radius: var(--border-radius-2xl);
|
border-radius: var(--border-radius-2xl);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: var(--shadow-2xl);
|
box-shadow: var(--shadow-2xl);
|
||||||
@ -603,10 +650,12 @@ body {
|
|||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: linear-gradient(135deg,
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
transparent 0%,
|
transparent 0%,
|
||||||
rgba(0, 0, 0, 0.1) 20%,
|
rgba(0, 0, 0, 0.1) 20%,
|
||||||
rgba(0, 0, 0, 0.2) 100%);
|
rgba(0, 0, 0, 0.2) 100%
|
||||||
|
);
|
||||||
border-radius: var(--border-radius-2xl);
|
border-radius: var(--border-radius-2xl);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -651,7 +700,15 @@ body {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
background: linear-gradient(45deg, var(--color-white) 25%, var(--color-gray-200) 25%, var(--color-gray-200) 50%, var(--color-white) 50%, var(--color-white) 75%, var(--color-gray-200) 75%);
|
background: linear-gradient(
|
||||||
|
45deg,
|
||||||
|
var(--color-white) 25%,
|
||||||
|
var(--color-gray-200) 25%,
|
||||||
|
var(--color-gray-200) 50%,
|
||||||
|
var(--color-white) 50%,
|
||||||
|
var(--color-white) 75%,
|
||||||
|
var(--color-gray-200) 75%
|
||||||
|
);
|
||||||
background-size: 5px 5px;
|
background-size: 5px 5px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
top: 45%;
|
top: 45%;
|
||||||
@ -736,7 +793,11 @@ body {
|
|||||||
.match-date {
|
.match-date {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-dark) 100%);
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
var(--color-primary) 0%,
|
||||||
|
var(--color-primary-dark) 100%
|
||||||
|
);
|
||||||
border-radius: var(--border-radius-lg);
|
border-radius: var(--border-radius-lg);
|
||||||
color: var(--color-white);
|
color: var(--color-white);
|
||||||
}
|
}
|
||||||
@ -806,7 +867,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.logo-nanjing::before {
|
.logo-nanjing::before {
|
||||||
content: 'N';
|
content: "N";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@ -822,7 +883,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.logo-suzhou::before {
|
.logo-suzhou::before {
|
||||||
content: 'S';
|
content: "S";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@ -1011,7 +1072,11 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.standings-table thead {
|
.standings-table thead {
|
||||||
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-dark) 100%);
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
var(--color-primary) 0%,
|
||||||
|
var(--color-primary-dark) 100%
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
.standings-table th {
|
.standings-table th {
|
||||||
@ -1407,22 +1472,28 @@ body {
|
|||||||
|
|
||||||
.news-card-image {
|
.news-card-image {
|
||||||
height: 200px;
|
height: 200px;
|
||||||
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-secondary) 100%);
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
var(--color-primary) 0%,
|
||||||
|
var(--color-secondary) 100%
|
||||||
|
);
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.news-card-image::before {
|
.news-card-image::before {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: linear-gradient(45deg,
|
background: linear-gradient(
|
||||||
|
45deg,
|
||||||
transparent 30%,
|
transparent 30%,
|
||||||
rgba(255, 255, 255, 0.1) 50%,
|
rgba(255, 255, 255, 0.1) 50%,
|
||||||
transparent 70%);
|
transparent 70%
|
||||||
|
);
|
||||||
animation: shimmer 2s infinite;
|
animation: shimmer 2s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1484,7 +1555,11 @@ body {
|
|||||||
|
|
||||||
/* 底部 */
|
/* 底部 */
|
||||||
.footer {
|
.footer {
|
||||||
background: linear-gradient(135deg, var(--color-gray-900) 0%, var(--color-black) 100%);
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
var(--color-gray-900) 0%,
|
||||||
|
var(--color-black) 100%
|
||||||
|
);
|
||||||
color: var(--color-white);
|
color: var(--color-white);
|
||||||
padding: 4rem 0 2rem;
|
padding: 4rem 0 2rem;
|
||||||
}
|
}
|
||||||
@ -1597,7 +1672,8 @@ body {
|
|||||||
|
|
||||||
/* 动画 */
|
/* 动画 */
|
||||||
@keyframes float {
|
@keyframes float {
|
||||||
0%, 100% {
|
0%,
|
||||||
|
100% {
|
||||||
transform: translateY(-50%) translateX(0);
|
transform: translateY(-50%) translateX(0);
|
||||||
}
|
}
|
||||||
50% {
|
50% {
|
||||||
@ -1606,7 +1682,8 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes player-move-1 {
|
@keyframes player-move-1 {
|
||||||
0%, 100% {
|
0%,
|
||||||
|
100% {
|
||||||
transform: translate(0, 0);
|
transform: translate(0, 0);
|
||||||
}
|
}
|
||||||
50% {
|
50% {
|
||||||
@ -1615,7 +1692,8 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes player-move-2 {
|
@keyframes player-move-2 {
|
||||||
0%, 100% {
|
0%,
|
||||||
|
100% {
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
}
|
}
|
||||||
50% {
|
50% {
|
||||||
@ -1624,7 +1702,8 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes player-move-3 {
|
@keyframes player-move-3 {
|
||||||
0%, 100% {
|
0%,
|
||||||
|
100% {
|
||||||
transform: translate(0, 0);
|
transform: translate(0, 0);
|
||||||
}
|
}
|
||||||
50% {
|
50% {
|
||||||
@ -1676,7 +1755,8 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes bounce {
|
@keyframes bounce {
|
||||||
0%, 100% {
|
0%,
|
||||||
|
100% {
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
50% {
|
50% {
|
||||||
@ -1685,7 +1765,8 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0%, 100% {
|
0%,
|
||||||
|
100% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
50% {
|
50% {
|
||||||
|
|||||||
@ -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>"
|
||||||
|
/>
|
||||||
|
|||||||
@ -1,18 +1,30 @@
|
|||||||
<!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">
|
||||||
@ -262,7 +274,9 @@
|
|||||||
|
|
||||||
<div class="stats-tabs">
|
<div class="stats-tabs">
|
||||||
<div class="stats-tab-nav">
|
<div class="stats-tab-nav">
|
||||||
<button class="stats-tab active" data-tab="scorers">射手榜</button>
|
<button class="stats-tab active" data-tab="scorers">
|
||||||
|
射手榜
|
||||||
|
</button>
|
||||||
<button class="stats-tab" data-tab="assists">助攻榜</button>
|
<button class="stats-tab" data-tab="assists">助攻榜</button>
|
||||||
<button class="stats-tab" data-tab="teams">球队数据</button>
|
<button class="stats-tab" data-tab="teams">球队数据</button>
|
||||||
</div>
|
</div>
|
||||||
@ -310,9 +324,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="footer-social">
|
<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-weibo"></i></a>
|
||||||
<a href="#" class="social-link"><i class="fab fa-weixin"></i></a>
|
<a href="#" class="social-link"
|
||||||
<a href="#" class="social-link"><i class="fab fa-douyin"></i></a>
|
><i class="fab fa-weixin"></i
|
||||||
<a href="#" class="social-link"><i class="fab fa-bilibili"></i></a>
|
></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>
|
</div>
|
||||||
|
|
||||||
@ -361,5 +381,5 @@
|
|||||||
<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>
|
||||||
@ -10,7 +10,7 @@ const leagueData = {
|
|||||||
totalMatches: 132,
|
totalMatches: 132,
|
||||||
weeks: 26,
|
weeks: 26,
|
||||||
startDate: "2025-03-01",
|
startDate: "2025-03-01",
|
||||||
endDate: "2025-10-31"
|
endDate: "2025-10-31",
|
||||||
},
|
},
|
||||||
|
|
||||||
// 参赛球队
|
// 参赛球队
|
||||||
@ -25,7 +25,7 @@ const leagueData = {
|
|||||||
stadium: "南京奥体中心",
|
stadium: "南京奥体中心",
|
||||||
capacity: 62000,
|
capacity: 62000,
|
||||||
manager: "张伟",
|
manager: "张伟",
|
||||||
captain: "李明"
|
captain: "李明",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
@ -37,7 +37,7 @@ const leagueData = {
|
|||||||
stadium: "苏州奥林匹克体育中心",
|
stadium: "苏州奥林匹克体育中心",
|
||||||
capacity: 45000,
|
capacity: 45000,
|
||||||
manager: "王强",
|
manager: "王强",
|
||||||
captain: "陈浩"
|
captain: "陈浩",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
@ -49,7 +49,7 @@ const leagueData = {
|
|||||||
stadium: "无锡体育中心",
|
stadium: "无锡体育中心",
|
||||||
capacity: 32000,
|
capacity: 32000,
|
||||||
manager: "赵刚",
|
manager: "赵刚",
|
||||||
captain: "刘洋"
|
captain: "刘洋",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 4,
|
id: 4,
|
||||||
@ -61,7 +61,7 @@ const leagueData = {
|
|||||||
stadium: "常州奥林匹克体育中心",
|
stadium: "常州奥林匹克体育中心",
|
||||||
capacity: 38000,
|
capacity: 38000,
|
||||||
manager: "孙磊",
|
manager: "孙磊",
|
||||||
captain: "周涛"
|
captain: "周涛",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 5,
|
id: 5,
|
||||||
@ -73,7 +73,7 @@ const leagueData = {
|
|||||||
stadium: "镇江体育会展中心",
|
stadium: "镇江体育会展中心",
|
||||||
capacity: 28000,
|
capacity: 28000,
|
||||||
manager: "吴斌",
|
manager: "吴斌",
|
||||||
captain: "郑军"
|
captain: "郑军",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 6,
|
id: 6,
|
||||||
@ -85,7 +85,7 @@ const leagueData = {
|
|||||||
stadium: "扬州体育公园",
|
stadium: "扬州体育公园",
|
||||||
capacity: 35000,
|
capacity: 35000,
|
||||||
manager: "钱勇",
|
manager: "钱勇",
|
||||||
captain: "王磊"
|
captain: "王磊",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 7,
|
id: 7,
|
||||||
@ -97,7 +97,7 @@ const leagueData = {
|
|||||||
stadium: "南通体育会展中心",
|
stadium: "南通体育会展中心",
|
||||||
capacity: 32000,
|
capacity: 32000,
|
||||||
manager: "冯超",
|
manager: "冯超",
|
||||||
captain: "张勇"
|
captain: "张勇",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 8,
|
id: 8,
|
||||||
@ -109,7 +109,7 @@ const leagueData = {
|
|||||||
stadium: "徐州奥体中心",
|
stadium: "徐州奥体中心",
|
||||||
capacity: 42000,
|
capacity: 42000,
|
||||||
manager: "陈明",
|
manager: "陈明",
|
||||||
captain: "李强"
|
captain: "李强",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 9,
|
id: 9,
|
||||||
@ -121,7 +121,7 @@ const leagueData = {
|
|||||||
stadium: "淮安体育中心",
|
stadium: "淮安体育中心",
|
||||||
capacity: 30000,
|
capacity: 30000,
|
||||||
manager: "周伟",
|
manager: "周伟",
|
||||||
captain: "吴刚"
|
captain: "吴刚",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 10,
|
id: 10,
|
||||||
@ -133,7 +133,7 @@ const leagueData = {
|
|||||||
stadium: "盐城体育中心",
|
stadium: "盐城体育中心",
|
||||||
capacity: 32000,
|
capacity: 32000,
|
||||||
manager: "郑涛",
|
manager: "郑涛",
|
||||||
captain: "孙明"
|
captain: "孙明",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 11,
|
id: 11,
|
||||||
@ -145,7 +145,7 @@ const leagueData = {
|
|||||||
stadium: "泰州体育公园",
|
stadium: "泰州体育公园",
|
||||||
capacity: 28000,
|
capacity: 28000,
|
||||||
manager: "王刚",
|
manager: "王刚",
|
||||||
captain: "陈涛"
|
captain: "陈涛",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 12,
|
id: 12,
|
||||||
@ -157,8 +157,8 @@ const leagueData = {
|
|||||||
stadium: "宿迁体育中心",
|
stadium: "宿迁体育中心",
|
||||||
capacity: 26000,
|
capacity: 26000,
|
||||||
manager: "李伟",
|
manager: "李伟",
|
||||||
captain: "张刚"
|
captain: "张刚",
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
// 积分榜数据
|
// 积分榜数据
|
||||||
@ -173,7 +173,7 @@ const leagueData = {
|
|||||||
goalsFor: 24,
|
goalsFor: 24,
|
||||||
goalsAgainst: 12,
|
goalsAgainst: 12,
|
||||||
goalDifference: 12,
|
goalDifference: 12,
|
||||||
points: 27
|
points: 27,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rank: 2,
|
rank: 2,
|
||||||
@ -185,7 +185,7 @@ const leagueData = {
|
|||||||
goalsFor: 22,
|
goalsFor: 22,
|
||||||
goalsAgainst: 14,
|
goalsAgainst: 14,
|
||||||
goalDifference: 8,
|
goalDifference: 8,
|
||||||
points: 25
|
points: 25,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rank: 3,
|
rank: 3,
|
||||||
@ -197,7 +197,7 @@ const leagueData = {
|
|||||||
goalsFor: 20,
|
goalsFor: 20,
|
||||||
goalsAgainst: 15,
|
goalsAgainst: 15,
|
||||||
goalDifference: 5,
|
goalDifference: 5,
|
||||||
points: 24
|
points: 24,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rank: 4,
|
rank: 4,
|
||||||
@ -209,7 +209,7 @@ const leagueData = {
|
|||||||
goalsFor: 18,
|
goalsFor: 18,
|
||||||
goalsAgainst: 14,
|
goalsAgainst: 14,
|
||||||
goalDifference: 4,
|
goalDifference: 4,
|
||||||
points: 22
|
points: 22,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rank: 5,
|
rank: 5,
|
||||||
@ -221,7 +221,7 @@ const leagueData = {
|
|||||||
goalsFor: 19,
|
goalsFor: 19,
|
||||||
goalsAgainst: 16,
|
goalsAgainst: 16,
|
||||||
goalDifference: 3,
|
goalDifference: 3,
|
||||||
points: 21
|
points: 21,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rank: 6,
|
rank: 6,
|
||||||
@ -233,7 +233,7 @@ const leagueData = {
|
|||||||
goalsFor: 17,
|
goalsFor: 17,
|
||||||
goalsAgainst: 15,
|
goalsAgainst: 15,
|
||||||
goalDifference: 2,
|
goalDifference: 2,
|
||||||
points: 20
|
points: 20,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rank: 7,
|
rank: 7,
|
||||||
@ -245,7 +245,7 @@ const leagueData = {
|
|||||||
goalsFor: 16,
|
goalsFor: 16,
|
||||||
goalsAgainst: 15,
|
goalsAgainst: 15,
|
||||||
goalDifference: 1,
|
goalDifference: 1,
|
||||||
points: 19
|
points: 19,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rank: 8,
|
rank: 8,
|
||||||
@ -257,7 +257,7 @@ const leagueData = {
|
|||||||
goalsFor: 15,
|
goalsFor: 15,
|
||||||
goalsAgainst: 16,
|
goalsAgainst: 16,
|
||||||
goalDifference: -1,
|
goalDifference: -1,
|
||||||
points: 17
|
points: 17,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rank: 9,
|
rank: 9,
|
||||||
@ -269,7 +269,7 @@ const leagueData = {
|
|||||||
goalsFor: 14,
|
goalsFor: 14,
|
||||||
goalsAgainst: 17,
|
goalsAgainst: 17,
|
||||||
goalDifference: -3,
|
goalDifference: -3,
|
||||||
points: 16
|
points: 16,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rank: 10,
|
rank: 10,
|
||||||
@ -281,7 +281,7 @@ const leagueData = {
|
|||||||
goalsFor: 13,
|
goalsFor: 13,
|
||||||
goalsAgainst: 18,
|
goalsAgainst: 18,
|
||||||
goalDifference: -5,
|
goalDifference: -5,
|
||||||
points: 14
|
points: 14,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rank: 11,
|
rank: 11,
|
||||||
@ -293,7 +293,7 @@ const leagueData = {
|
|||||||
goalsFor: 11,
|
goalsFor: 11,
|
||||||
goalsAgainst: 20,
|
goalsAgainst: 20,
|
||||||
goalDifference: -9,
|
goalDifference: -9,
|
||||||
points: 10
|
points: 10,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rank: 12,
|
rank: 12,
|
||||||
@ -305,8 +305,8 @@ const leagueData = {
|
|||||||
goalsFor: 9,
|
goalsFor: 9,
|
||||||
goalsAgainst: 24,
|
goalsAgainst: 24,
|
||||||
goalDifference: -15,
|
goalDifference: -15,
|
||||||
points: 6
|
points: 6,
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
// 赛程数据
|
// 赛程数据
|
||||||
@ -321,7 +321,7 @@ const leagueData = {
|
|||||||
venue: "南京奥体中心",
|
venue: "南京奥体中心",
|
||||||
status: "completed",
|
status: "completed",
|
||||||
homeScore: 2,
|
homeScore: 2,
|
||||||
awayScore: 1
|
awayScore: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
@ -333,7 +333,7 @@ const leagueData = {
|
|||||||
venue: "无锡体育中心",
|
venue: "无锡体育中心",
|
||||||
status: "completed",
|
status: "completed",
|
||||||
homeScore: 1,
|
homeScore: 1,
|
||||||
awayScore: 1
|
awayScore: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
@ -345,7 +345,7 @@ const leagueData = {
|
|||||||
venue: "镇江体育会展中心",
|
venue: "镇江体育会展中心",
|
||||||
status: "completed",
|
status: "completed",
|
||||||
homeScore: 0,
|
homeScore: 0,
|
||||||
awayScore: 2
|
awayScore: 2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 4,
|
id: 4,
|
||||||
@ -357,7 +357,7 @@ const leagueData = {
|
|||||||
venue: "南通体育会展中心",
|
venue: "南通体育会展中心",
|
||||||
status: "completed",
|
status: "completed",
|
||||||
homeScore: 1,
|
homeScore: 1,
|
||||||
awayScore: 3
|
awayScore: 3,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 5,
|
id: 5,
|
||||||
@ -369,7 +369,7 @@ const leagueData = {
|
|||||||
venue: "淮安体育中心",
|
venue: "淮安体育中心",
|
||||||
status: "completed",
|
status: "completed",
|
||||||
homeScore: 2,
|
homeScore: 2,
|
||||||
awayScore: 2
|
awayScore: 2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 6,
|
id: 6,
|
||||||
@ -381,7 +381,7 @@ const leagueData = {
|
|||||||
venue: "泰州体育公园",
|
venue: "泰州体育公园",
|
||||||
status: "completed",
|
status: "completed",
|
||||||
homeScore: 1,
|
homeScore: 1,
|
||||||
awayScore: 0
|
awayScore: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 7,
|
id: 7,
|
||||||
@ -393,7 +393,7 @@ const leagueData = {
|
|||||||
venue: "苏州奥林匹克体育中心",
|
venue: "苏州奥林匹克体育中心",
|
||||||
status: "completed",
|
status: "completed",
|
||||||
homeScore: 2,
|
homeScore: 2,
|
||||||
awayScore: 0
|
awayScore: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 8,
|
id: 8,
|
||||||
@ -405,7 +405,7 @@ const leagueData = {
|
|||||||
venue: "常州奥林匹克体育中心",
|
venue: "常州奥林匹克体育中心",
|
||||||
status: "completed",
|
status: "completed",
|
||||||
homeScore: 3,
|
homeScore: 3,
|
||||||
awayScore: 1
|
awayScore: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 9,
|
id: 9,
|
||||||
@ -417,7 +417,7 @@ const leagueData = {
|
|||||||
venue: "扬州体育公园",
|
venue: "扬州体育公园",
|
||||||
status: "completed",
|
status: "completed",
|
||||||
homeScore: 1,
|
homeScore: 1,
|
||||||
awayScore: 1
|
awayScore: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 10,
|
id: 10,
|
||||||
@ -429,7 +429,7 @@ const leagueData = {
|
|||||||
venue: "徐州奥体中心",
|
venue: "徐州奥体中心",
|
||||||
status: "completed",
|
status: "completed",
|
||||||
homeScore: 2,
|
homeScore: 2,
|
||||||
awayScore: 0
|
awayScore: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 11,
|
id: 11,
|
||||||
@ -441,7 +441,7 @@ const leagueData = {
|
|||||||
venue: "盐城体育中心",
|
venue: "盐城体育中心",
|
||||||
status: "completed",
|
status: "completed",
|
||||||
homeScore: 1,
|
homeScore: 1,
|
||||||
awayScore: 0
|
awayScore: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 12,
|
id: 12,
|
||||||
@ -453,7 +453,7 @@ const leagueData = {
|
|||||||
venue: "宿迁体育中心",
|
venue: "宿迁体育中心",
|
||||||
status: "completed",
|
status: "completed",
|
||||||
homeScore: 0,
|
homeScore: 0,
|
||||||
awayScore: 3
|
awayScore: 3,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 13,
|
id: 13,
|
||||||
@ -463,7 +463,7 @@ const leagueData = {
|
|||||||
homeTeamId: 1,
|
homeTeamId: 1,
|
||||||
awayTeamId: 2,
|
awayTeamId: 2,
|
||||||
venue: "南京奥体中心",
|
venue: "南京奥体中心",
|
||||||
status: "scheduled"
|
status: "scheduled",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 14,
|
id: 14,
|
||||||
@ -473,7 +473,7 @@ const leagueData = {
|
|||||||
homeTeamId: 3,
|
homeTeamId: 3,
|
||||||
awayTeamId: 4,
|
awayTeamId: 4,
|
||||||
venue: "无锡体育中心",
|
venue: "无锡体育中心",
|
||||||
status: "scheduled"
|
status: "scheduled",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 15,
|
id: 15,
|
||||||
@ -483,7 +483,7 @@ const leagueData = {
|
|||||||
homeTeamId: 5,
|
homeTeamId: 5,
|
||||||
awayTeamId: 6,
|
awayTeamId: 6,
|
||||||
venue: "镇江体育会展中心",
|
venue: "镇江体育会展中心",
|
||||||
status: "scheduled"
|
status: "scheduled",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 16,
|
id: 16,
|
||||||
@ -493,7 +493,7 @@ const leagueData = {
|
|||||||
homeTeamId: 7,
|
homeTeamId: 7,
|
||||||
awayTeamId: 8,
|
awayTeamId: 8,
|
||||||
venue: "南通体育会展中心",
|
venue: "南通体育会展中心",
|
||||||
status: "scheduled"
|
status: "scheduled",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 17,
|
id: 17,
|
||||||
@ -503,7 +503,7 @@ const leagueData = {
|
|||||||
homeTeamId: 9,
|
homeTeamId: 9,
|
||||||
awayTeamId: 10,
|
awayTeamId: 10,
|
||||||
venue: "淮安体育中心",
|
venue: "淮安体育中心",
|
||||||
status: "scheduled"
|
status: "scheduled",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 18,
|
id: 18,
|
||||||
@ -513,8 +513,8 @@ const leagueData = {
|
|||||||
homeTeamId: 11,
|
homeTeamId: 11,
|
||||||
awayTeamId: 12,
|
awayTeamId: 12,
|
||||||
venue: "泰州体育公园",
|
venue: "泰州体育公园",
|
||||||
status: "scheduled"
|
status: "scheduled",
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
// 球员数据
|
// 球员数据
|
||||||
@ -528,7 +528,7 @@ const leagueData = {
|
|||||||
goals: 12,
|
goals: 12,
|
||||||
assists: 4,
|
assists: 4,
|
||||||
matches: 13,
|
matches: 13,
|
||||||
minutes: 1170
|
minutes: 1170,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rank: 2,
|
rank: 2,
|
||||||
@ -538,7 +538,7 @@ const leagueData = {
|
|||||||
goals: 8,
|
goals: 8,
|
||||||
assists: 6,
|
assists: 6,
|
||||||
matches: 13,
|
matches: 13,
|
||||||
minutes: 1170
|
minutes: 1170,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rank: 3,
|
rank: 3,
|
||||||
@ -548,7 +548,7 @@ const leagueData = {
|
|||||||
goals: 7,
|
goals: 7,
|
||||||
assists: 5,
|
assists: 5,
|
||||||
matches: 13,
|
matches: 13,
|
||||||
minutes: 1170
|
minutes: 1170,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rank: 4,
|
rank: 4,
|
||||||
@ -558,7 +558,7 @@ const leagueData = {
|
|||||||
goals: 6,
|
goals: 6,
|
||||||
assists: 3,
|
assists: 3,
|
||||||
matches: 13,
|
matches: 13,
|
||||||
minutes: 1170
|
minutes: 1170,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rank: 5,
|
rank: 5,
|
||||||
@ -568,7 +568,7 @@ const leagueData = {
|
|||||||
goals: 6,
|
goals: 6,
|
||||||
assists: 2,
|
assists: 2,
|
||||||
matches: 13,
|
matches: 13,
|
||||||
minutes: 1170
|
minutes: 1170,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rank: 6,
|
rank: 6,
|
||||||
@ -578,7 +578,7 @@ const leagueData = {
|
|||||||
goals: 5,
|
goals: 5,
|
||||||
assists: 4,
|
assists: 4,
|
||||||
matches: 13,
|
matches: 13,
|
||||||
minutes: 1170
|
minutes: 1170,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rank: 7,
|
rank: 7,
|
||||||
@ -588,7 +588,7 @@ const leagueData = {
|
|||||||
goals: 5,
|
goals: 5,
|
||||||
assists: 3,
|
assists: 3,
|
||||||
matches: 13,
|
matches: 13,
|
||||||
minutes: 1170
|
minutes: 1170,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rank: 8,
|
rank: 8,
|
||||||
@ -598,7 +598,7 @@ const leagueData = {
|
|||||||
goals: 4,
|
goals: 4,
|
||||||
assists: 5,
|
assists: 5,
|
||||||
matches: 13,
|
matches: 13,
|
||||||
minutes: 1170
|
minutes: 1170,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rank: 9,
|
rank: 9,
|
||||||
@ -608,7 +608,7 @@ const leagueData = {
|
|||||||
goals: 4,
|
goals: 4,
|
||||||
assists: 3,
|
assists: 3,
|
||||||
matches: 13,
|
matches: 13,
|
||||||
minutes: 1170
|
minutes: 1170,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rank: 10,
|
rank: 10,
|
||||||
@ -618,8 +618,8 @@ const leagueData = {
|
|||||||
goals: 3,
|
goals: 3,
|
||||||
assists: 2,
|
assists: 2,
|
||||||
matches: 13,
|
matches: 13,
|
||||||
minutes: 1170
|
minutes: 1170,
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
assists: [
|
assists: [
|
||||||
@ -631,7 +631,7 @@ const leagueData = {
|
|||||||
assists: 6,
|
assists: 6,
|
||||||
goals: 8,
|
goals: 8,
|
||||||
matches: 13,
|
matches: 13,
|
||||||
minutes: 1170
|
minutes: 1170,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rank: 2,
|
rank: 2,
|
||||||
@ -641,7 +641,7 @@ const leagueData = {
|
|||||||
assists: 5,
|
assists: 5,
|
||||||
goals: 4,
|
goals: 4,
|
||||||
matches: 13,
|
matches: 13,
|
||||||
minutes: 1170
|
minutes: 1170,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rank: 3,
|
rank: 3,
|
||||||
@ -651,7 +651,7 @@ const leagueData = {
|
|||||||
assists: 5,
|
assists: 5,
|
||||||
goals: 7,
|
goals: 7,
|
||||||
matches: 13,
|
matches: 13,
|
||||||
minutes: 1170
|
minutes: 1170,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rank: 4,
|
rank: 4,
|
||||||
@ -661,7 +661,7 @@ const leagueData = {
|
|||||||
assists: 4,
|
assists: 4,
|
||||||
goals: 5,
|
goals: 5,
|
||||||
matches: 13,
|
matches: 13,
|
||||||
minutes: 1170
|
minutes: 1170,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rank: 5,
|
rank: 5,
|
||||||
@ -671,7 +671,7 @@ const leagueData = {
|
|||||||
assists: 4,
|
assists: 4,
|
||||||
goals: 12,
|
goals: 12,
|
||||||
matches: 13,
|
matches: 13,
|
||||||
minutes: 1170
|
minutes: 1170,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rank: 6,
|
rank: 6,
|
||||||
@ -681,7 +681,7 @@ const leagueData = {
|
|||||||
assists: 3,
|
assists: 3,
|
||||||
goals: 6,
|
goals: 6,
|
||||||
matches: 13,
|
matches: 13,
|
||||||
minutes: 1170
|
minutes: 1170,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rank: 7,
|
rank: 7,
|
||||||
@ -691,7 +691,7 @@ const leagueData = {
|
|||||||
assists: 3,
|
assists: 3,
|
||||||
goals: 5,
|
goals: 5,
|
||||||
matches: 13,
|
matches: 13,
|
||||||
minutes: 1170
|
minutes: 1170,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rank: 8,
|
rank: 8,
|
||||||
@ -701,7 +701,7 @@ const leagueData = {
|
|||||||
assists: 3,
|
assists: 3,
|
||||||
goals: 4,
|
goals: 4,
|
||||||
matches: 13,
|
matches: 13,
|
||||||
minutes: 1170
|
minutes: 1170,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rank: 9,
|
rank: 9,
|
||||||
@ -711,7 +711,7 @@ const leagueData = {
|
|||||||
assists: 3,
|
assists: 3,
|
||||||
goals: 2,
|
goals: 2,
|
||||||
matches: 13,
|
matches: 13,
|
||||||
minutes: 1170
|
minutes: 1170,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rank: 10,
|
rank: 10,
|
||||||
@ -721,9 +721,9 @@ const leagueData = {
|
|||||||
assists: 2,
|
assists: 2,
|
||||||
goals: 1,
|
goals: 1,
|
||||||
matches: 13,
|
matches: 13,
|
||||||
minutes: 1170
|
minutes: 1170,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
// 新闻数据
|
// 新闻数据
|
||||||
@ -731,64 +731,70 @@ const leagueData = {
|
|||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
title: "南京城联主场力克苏州雄狮,继续领跑积分榜",
|
title: "南京城联主场力克苏州雄狮,继续领跑积分榜",
|
||||||
excerpt: "在昨晚进行的第12轮焦点战中,南京城联凭借张伟的梅开二度,主场2-1战胜苏州雄狮,继续以2分优势领跑积分榜。",
|
excerpt:
|
||||||
|
"在昨晚进行的第12轮焦点战中,南京城联凭借张伟的梅开二度,主场2-1战胜苏州雄狮,继续以2分优势领跑积分榜。",
|
||||||
category: "比赛战报",
|
category: "比赛战报",
|
||||||
date: "2025-05-25",
|
date: "2025-05-25",
|
||||||
imageColor: "#dc2626"
|
imageColor: "#dc2626",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
title: "联赛最佳球员揭晓:张伟当选4月最佳",
|
title: "联赛最佳球员揭晓:张伟当选4月最佳",
|
||||||
excerpt: "江苏城市足球联赛官方宣布,南京城联前锋张伟凭借出色的表现,当选4月份联赛最佳球员。",
|
excerpt:
|
||||||
|
"江苏城市足球联赛官方宣布,南京城联前锋张伟凭借出色的表现,当选4月份联赛最佳球员。",
|
||||||
category: "官方公告",
|
category: "官方公告",
|
||||||
date: "2025-05-20",
|
date: "2025-05-20",
|
||||||
imageColor: "#3b82f6"
|
imageColor: "#3b82f6",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
title: "徐州楚汉签下前国脚李强,实力大增",
|
title: "徐州楚汉签下前国脚李强,实力大增",
|
||||||
excerpt: "徐州楚汉俱乐部官方宣布,与前国家队中场李强签约两年,这位经验丰富的老将将提升球队中场实力。",
|
excerpt:
|
||||||
|
"徐州楚汉俱乐部官方宣布,与前国家队中场李强签约两年,这位经验丰富的老将将提升球队中场实力。",
|
||||||
category: "转会新闻",
|
category: "转会新闻",
|
||||||
date: "2025-05-18",
|
date: "2025-05-18",
|
||||||
imageColor: "#84cc16"
|
imageColor: "#84cc16",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 4,
|
id: 4,
|
||||||
title: "联赛半程总结:竞争激烈,多队有望争冠",
|
title: "联赛半程总结:竞争激烈,多队有望争冠",
|
||||||
excerpt: "随着联赛进入半程,积分榜前六名球队分差仅7分,本赛季冠军争夺异常激烈,多支球队都有机会问鼎。",
|
excerpt:
|
||||||
|
"随着联赛进入半程,积分榜前六名球队分差仅7分,本赛季冠军争夺异常激烈,多支球队都有机会问鼎。",
|
||||||
category: "联赛动态",
|
category: "联赛动态",
|
||||||
date: "2025-05-15",
|
date: "2025-05-15",
|
||||||
imageColor: "#f59e0b"
|
imageColor: "#f59e0b",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 5,
|
id: 5,
|
||||||
title: "球迷互动日:各俱乐部将举办开放训练",
|
title: "球迷互动日:各俱乐部将举办开放训练",
|
||||||
excerpt: "为感谢球迷支持,各俱乐部将在本周末举办球迷开放日,球迷可近距离观看球队训练并与球员互动。",
|
excerpt:
|
||||||
|
"为感谢球迷支持,各俱乐部将在本周末举办球迷开放日,球迷可近距离观看球队训练并与球员互动。",
|
||||||
category: "球迷活动",
|
category: "球迷活动",
|
||||||
date: "2025-05-12",
|
date: "2025-05-12",
|
||||||
imageColor: "#ec4899"
|
imageColor: "#ec4899",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 6,
|
id: 6,
|
||||||
title: "技术统计:联赛进球数创历史新高",
|
title: "技术统计:联赛进球数创历史新高",
|
||||||
excerpt: "本赛季前13轮共打进176球,场均2.77球,创下联赛历史同期最高进球纪录,进攻足球成为主流。",
|
excerpt:
|
||||||
|
"本赛季前13轮共打进176球,场均2.77球,创下联赛历史同期最高进球纪录,进攻足球成为主流。",
|
||||||
category: "数据统计",
|
category: "数据统计",
|
||||||
date: "2025-05-10",
|
date: "2025-05-10",
|
||||||
imageColor: "#0ea5e9"
|
imageColor: "#0ea5e9",
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
// 工具函数:根据ID获取球队信息
|
// 工具函数:根据ID获取球队信息
|
||||||
function getTeamById(teamId) {
|
function getTeamById(teamId) {
|
||||||
return leagueData.teams.find(team => team.id === teamId);
|
return leagueData.teams.find((team) => team.id === teamId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 工具函数:格式化日期
|
// 工具函数:格式化日期
|
||||||
function formatDate(dateString) {
|
function formatDate(dateString) {
|
||||||
const date = new Date(dateString);
|
const date = new Date(dateString);
|
||||||
const options = { weekday: 'short', month: 'short', day: 'numeric' };
|
const options = { weekday: "short", month: "short", day: "numeric" };
|
||||||
return date.toLocaleDateString('zh-CN', options);
|
return date.toLocaleDateString("zh-CN", options);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 工具函数:格式化时间
|
// 工具函数:格式化时间
|
||||||
@ -797,6 +803,6 @@ function formatTime(timeString) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 导出数据
|
// 导出数据
|
||||||
if (typeof module !== 'undefined' && module.exports) {
|
if (typeof module !== "undefined" && module.exports) {
|
||||||
module.exports = leagueData;
|
module.exports = leagueData;
|
||||||
}
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
// 江苏城市足球联赛2025赛季 - 主JavaScript文件
|
// 江苏城市足球联赛2025赛季 - 主JavaScript文件
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
// 初始化加载动画
|
// 初始化加载动画
|
||||||
initLoader();
|
initLoader();
|
||||||
|
|
||||||
@ -37,79 +37,79 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
// 加载动画
|
// 加载动画
|
||||||
function initLoader() {
|
function initLoader() {
|
||||||
const loader = document.querySelector('.loader');
|
const loader = document.querySelector(".loader");
|
||||||
|
|
||||||
// 模拟加载延迟
|
// 模拟加载延迟
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
loader.classList.add('loaded');
|
loader.classList.add("loaded");
|
||||||
|
|
||||||
// 动画结束后隐藏loader
|
// 动画结束后隐藏loader
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
loader.style.display = 'none';
|
loader.style.display = "none";
|
||||||
}, 300);
|
}, 300);
|
||||||
}, 1500);
|
}, 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');
|
const navMenu = document.querySelector(".nav-menu");
|
||||||
if (navMenu.classList.contains('active')) {
|
if (navMenu.classList.contains("active")) {
|
||||||
navMenu.classList.remove('active');
|
navMenu.classList.remove("active");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -118,25 +118,25 @@ function initNavigation() {
|
|||||||
|
|
||||||
// 滚动监听
|
// 滚动监听
|
||||||
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 => {
|
navLinks.forEach((link) => {
|
||||||
link.classList.remove('active');
|
link.classList.remove("active");
|
||||||
if (link.getAttribute('href') === `#${current}`) {
|
if (link.getAttribute("href") === `#${current}`) {
|
||||||
link.classList.add('active');
|
link.classList.add("active");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -144,18 +144,18 @@ function initScrollSpy() {
|
|||||||
|
|
||||||
// 渲染球队卡片
|
// 渲染球队卡片
|
||||||
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%);">
|
||||||
@ -165,21 +165,21 @@ 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} 的详细信息`);
|
||||||
});
|
});
|
||||||
@ -190,24 +190,24 @@ function renderTeams() {
|
|||||||
|
|
||||||
// 渲染积分榜
|
// 渲染积分榜
|
||||||
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 = `
|
||||||
@ -224,7 +224,7 @@ 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>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -234,15 +234,15 @@ function renderStandings() {
|
|||||||
|
|
||||||
// 渲染赛程表
|
// 渲染赛程表
|
||||||
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] = [];
|
||||||
}
|
}
|
||||||
@ -250,32 +250,42 @@ function renderFixtures() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 渲染所有赛程
|
// 渲染所有赛程
|
||||||
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) => {
|
||||||
|
const roundHeader = document.createElement("div");
|
||||||
|
roundHeader.className = "fixture-round-header";
|
||||||
roundHeader.innerHTML = `<h3>第${round}轮</h3>`;
|
roundHeader.innerHTML = `<h3>第${round}轮</h3>`;
|
||||||
fixturesList.appendChild(roundHeader);
|
fixturesList.appendChild(roundHeader);
|
||||||
|
|
||||||
fixturesByRound[round].forEach(fixture => {
|
fixturesByRound[round].forEach((fixture) => {
|
||||||
const homeTeam = getTeamById(fixture.homeTeamId);
|
const homeTeam = getTeamById(fixture.homeTeamId);
|
||||||
const awayTeam = getTeamById(fixture.awayTeamId);
|
const awayTeam = getTeamById(fixture.awayTeamId);
|
||||||
|
|
||||||
const fixtureItem = document.createElement('div');
|
const fixtureItem = document.createElement("div");
|
||||||
fixtureItem.className = 'fixture-item';
|
fixtureItem.className = "fixture-item";
|
||||||
|
|
||||||
const date = new Date(fixture.date);
|
const date = new Date(fixture.date);
|
||||||
const dayNames = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
|
const dayNames = [
|
||||||
|
"周日",
|
||||||
|
"周一",
|
||||||
|
"周二",
|
||||||
|
"周三",
|
||||||
|
"周四",
|
||||||
|
"周五",
|
||||||
|
"周六",
|
||||||
|
];
|
||||||
const dayName = dayNames[date.getDay()];
|
const dayName = dayNames[date.getDay()];
|
||||||
|
|
||||||
let scoreHtml = '';
|
let scoreHtml = "";
|
||||||
let statusText = '';
|
let statusText = "";
|
||||||
|
|
||||||
if (fixture.status === 'completed') {
|
if (fixture.status === "completed") {
|
||||||
scoreHtml = `
|
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>
|
||||||
@ -321,7 +331,7 @@ function renderStats() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderScorers() {
|
function renderScorers() {
|
||||||
const scorersContainer = document.querySelector('#scorers');
|
const scorersContainer = document.querySelector("#scorers");
|
||||||
|
|
||||||
if (!scorersContainer) return;
|
if (!scorersContainer) return;
|
||||||
|
|
||||||
@ -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,14 +361,15 @@ 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;
|
||||||
|
|
||||||
@ -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,22 +399,26 @@ 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
|
||||||
|
.map((standing) => {
|
||||||
const team = getTeamById(standing.teamId);
|
const team = getTeamById(standing.teamId);
|
||||||
const goalsPerGame = (standing.goalsFor / standing.played).toFixed(2);
|
const goalsPerGame = (standing.goalsFor / standing.played).toFixed(2);
|
||||||
const concededPerGame = (standing.goalsAgainst / standing.played).toFixed(2);
|
const concededPerGame = (standing.goalsAgainst / standing.played).toFixed(
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rank: standing.rank,
|
rank: standing.rank,
|
||||||
@ -411,9 +428,10 @@ function renderTeamStats() {
|
|||||||
goalDifference: standing.goalDifference,
|
goalDifference: standing.goalDifference,
|
||||||
goalsPerGame,
|
goalsPerGame,
|
||||||
concededPerGame,
|
concededPerGame,
|
||||||
cleanSheets: Math.floor(Math.random() * 5) // 模拟数据
|
cleanSheets: Math.floor(Math.random() * 5), // 模拟数据
|
||||||
};
|
};
|
||||||
}).sort((a, b) => a.rank - b.rank);
|
})
|
||||||
|
.sort((a, b) => a.rank - b.rank);
|
||||||
|
|
||||||
teamStatsContainer.innerHTML = `
|
teamStatsContainer.innerHTML = `
|
||||||
<table class="stats-table">
|
<table class="stats-table">
|
||||||
@ -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,21 +471,21 @@ 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 = `
|
||||||
@ -482,7 +504,7 @@ function renderNews() {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
newsCard.addEventListener('click', () => {
|
newsCard.addEventListener("click", () => {
|
||||||
alert(`查看新闻: ${newsItem.title}`);
|
alert(`查看新闻: ${newsItem.title}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -493,16 +515,16 @@ function renderNews() {
|
|||||||
// 初始化标签页切换
|
// 初始化标签页切换
|
||||||
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");
|
||||||
|
|
||||||
// 这里可以根据筛选条件显示不同的赛程
|
// 这里可以根据筛选条件显示不同的赛程
|
||||||
// 由于时间关系,这里只是简单的演示
|
// 由于时间关系,这里只是简单的演示
|
||||||
@ -511,22 +533,22 @@ function initTabs() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 数据统计标签页
|
// 数据统计标签页
|
||||||
const statsTabs = document.querySelectorAll('.stats-tab');
|
const statsTabs = document.querySelectorAll(".stats-tab");
|
||||||
const statsContents = document.querySelectorAll('.stats-tab-content');
|
const statsContents = document.querySelectorAll(".stats-tab-content");
|
||||||
|
|
||||||
statsTabs.forEach(tab => {
|
statsTabs.forEach((tab) => {
|
||||||
tab.addEventListener('click', () => {
|
tab.addEventListener("click", () => {
|
||||||
const tabId = tab.getAttribute('data-tab');
|
const tabId = tab.getAttribute("data-tab");
|
||||||
|
|
||||||
// 更新活动标签
|
// 更新活动标签
|
||||||
statsTabs.forEach(t => t.classList.remove('active'));
|
statsTabs.forEach((t) => t.classList.remove("active"));
|
||||||
tab.classList.add('active');
|
tab.classList.add("active");
|
||||||
|
|
||||||
// 显示对应内容
|
// 显示对应内容
|
||||||
statsContents.forEach(content => {
|
statsContents.forEach((content) => {
|
||||||
content.classList.remove('active');
|
content.classList.remove("active");
|
||||||
if (content.id === tabId) {
|
if (content.id === tabId) {
|
||||||
content.classList.add('active');
|
content.classList.add("active");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -535,27 +557,27 @@ function initTabs() {
|
|||||||
|
|
||||||
// 初始化移动端菜单
|
// 初始化移动端菜单
|
||||||
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";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -566,15 +588,20 @@ 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 +
|
0x1000000 +
|
||||||
(R < 255 ? R < 1 ? 0 : R : 255) * 0x10000 +
|
(R < 255 ? (R < 1 ? 0 : R) : 255) * 0x10000 +
|
||||||
(G < 255 ? G < 1 ? 0 : G : 255) * 0x100 +
|
(G < 255 ? (G < 1 ? 0 : G) : 255) * 0x100 +
|
||||||
(B < 255 ? B < 1 ? 0 : B : 255)
|
(B < 255 ? (B < 1 ? 0 : B) : 255)
|
||||||
).toString(16).slice(1);
|
)
|
||||||
|
.toString(16)
|
||||||
|
.slice(1)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 工具函数:格式化日期(简写)
|
// 工具函数:格式化日期(简写)
|
||||||
@ -587,32 +614,32 @@ function formatDate(dateString) {
|
|||||||
|
|
||||||
// 工具函数:根据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.addEventListener("mouseup", () => {
|
||||||
button.style.transform = '';
|
button.style.transform = "";
|
||||||
});
|
});
|
||||||
|
|
||||||
button.addEventListener('mouseleave', () => {
|
button.addEventListener("mouseleave", () => {
|
||||||
button.style.transform = '';
|
button.style.transform = "";
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 为卡片添加悬停效果
|
// 为卡片添加悬停效果
|
||||||
const cards = document.querySelectorAll('.team-card, .news-card');
|
const cards = document.querySelectorAll(".team-card, .news-card");
|
||||||
cards.forEach(card => {
|
cards.forEach((card) => {
|
||||||
card.addEventListener('mouseenter', () => {
|
card.addEventListener("mouseenter", () => {
|
||||||
card.style.transition = 'transform 0.3s ease, box-shadow 0.3s ease';
|
card.style.transition = "transform 0.3s ease, box-shadow 0.3s ease";
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -1,17 +1,27 @@
|
|||||||
<!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">
|
||||||
@ -36,7 +46,10 @@
|
|||||||
<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">
|
||||||
|
A comprehensive analysis of trends, opportunities, and challenges
|
||||||
|
shaping 2026
|
||||||
|
</p>
|
||||||
<div class="hero-stats">
|
<div class="hero-stats">
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<span class="stat-number">5</span>
|
<span class="stat-number">5</span>
|
||||||
@ -51,7 +64,9 @@
|
|||||||
<span class="stat-label">Technology Shifts</span>
|
<span class="stat-label">Technology Shifts</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<a href="#trends" class="cta-button">Explore Trends <i class="fas fa-arrow-down"></i></a>
|
<a href="#trends" class="cta-button"
|
||||||
|
>Explore Trends <i class="fas fa-arrow-down"></i
|
||||||
|
></a>
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-visual">
|
<div class="hero-visual">
|
||||||
<div class="visual-element">
|
<div class="visual-element">
|
||||||
@ -68,12 +83,25 @@
|
|||||||
<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">
|
||||||
|
Convergence, complexity, and unprecedented opportunities
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="overview-content">
|
<div class="overview-content">
|
||||||
<div class="overview-text">
|
<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>
|
||||||
<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>
|
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>
|
||||||
<div class="overview-highlight">
|
<div class="overview-highlight">
|
||||||
<div class="highlight-card">
|
<div class="highlight-card">
|
||||||
@ -81,14 +109,20 @@
|
|||||||
<i class="fas fa-brain"></i>
|
<i class="fas fa-brain"></i>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="highlight-title">AI Maturation</h3>
|
<h3 class="highlight-title">AI Maturation</h3>
|
||||||
<p class="highlight-text">Transition from experimentation to production deployment with autonomous agents</p>
|
<p class="highlight-text">
|
||||||
|
Transition from experimentation to production deployment with
|
||||||
|
autonomous agents
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="highlight-card">
|
<div class="highlight-card">
|
||||||
<div class="highlight-icon">
|
<div class="highlight-icon">
|
||||||
<i class="fas fa-leaf"></i>
|
<i class="fas fa-leaf"></i>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="highlight-title">Sustainability Focus</h3>
|
<h3 class="highlight-title">Sustainability Focus</h3>
|
||||||
<p class="highlight-text">Climate tech emerges as a dominant investment category with material financial implications</p>
|
<p class="highlight-text">
|
||||||
|
Climate tech emerges as a dominant investment category with
|
||||||
|
material financial implications
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -100,13 +134,17 @@
|
|||||||
<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">
|
||||||
|
Critical developments across technology, economy, and society
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="trends-grid">
|
<div class="trends-grid">
|
||||||
<!-- Technology Trends -->
|
<!-- Technology Trends -->
|
||||||
<div class="trend-category">
|
<div class="trend-category">
|
||||||
<h3 class="category-title"><i class="fas fa-microchip"></i> Technology & Innovation</h3>
|
<h3 class="category-title">
|
||||||
|
<i class="fas fa-microchip"></i> Technology & Innovation
|
||||||
|
</h3>
|
||||||
<div class="trend-cards">
|
<div class="trend-cards">
|
||||||
<div class="trend-card">
|
<div class="trend-card">
|
||||||
<div class="trend-header">
|
<div class="trend-header">
|
||||||
@ -114,10 +152,18 @@
|
|||||||
<span class="trend-priority high">High Impact</span>
|
<span class="trend-priority high">High Impact</span>
|
||||||
</div>
|
</div>
|
||||||
<h4 class="trend-name">AI Agents Proliferation</h4>
|
<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>
|
<p class="trend-description">
|
||||||
|
Autonomous AI agents become mainstream in enterprise
|
||||||
|
operations, requiring sophisticated governance frameworks and
|
||||||
|
security considerations.
|
||||||
|
</p>
|
||||||
<div class="trend-metrics">
|
<div class="trend-metrics">
|
||||||
<span class="metric"><i class="fas fa-rocket"></i> Exponential Growth</span>
|
<span class="metric"
|
||||||
<span class="metric"><i class="fas fa-shield-alt"></i> Security Critical</span>
|
><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>
|
</div>
|
||||||
|
|
||||||
@ -127,10 +173,18 @@
|
|||||||
<span class="trend-priority medium">Emerging</span>
|
<span class="trend-priority medium">Emerging</span>
|
||||||
</div>
|
</div>
|
||||||
<h4 class="trend-name">Quantum-AI Convergence</h4>
|
<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>
|
<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">
|
<div class="trend-metrics">
|
||||||
<span class="metric"><i class="fas fa-chart-line"></i> 18% Revenue Share</span>
|
<span class="metric"
|
||||||
<span class="metric"><i class="fas fa-cogs"></i> Optimization Focus</span>
|
><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>
|
</div>
|
||||||
|
|
||||||
@ -140,10 +194,18 @@
|
|||||||
<span class="trend-priority high">Critical</span>
|
<span class="trend-priority high">Critical</span>
|
||||||
</div>
|
</div>
|
||||||
<h4 class="trend-name">AI-Powered Cybersecurity</h4>
|
<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>
|
<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">
|
<div class="trend-metrics">
|
||||||
<span class="metric"><i class="fas fa-bolt"></i> Machine Speed</span>
|
<span class="metric"
|
||||||
<span class="metric"><i class="fas fa-user-shield"></i> Proactive Defense</span>
|
><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>
|
||||||
</div>
|
</div>
|
||||||
@ -151,7 +213,9 @@
|
|||||||
|
|
||||||
<!-- Economic Trends -->
|
<!-- Economic Trends -->
|
||||||
<div class="trend-category">
|
<div class="trend-category">
|
||||||
<h3 class="category-title"><i class="fas fa-chart-line"></i> Economic & Global</h3>
|
<h3 class="category-title">
|
||||||
|
<i class="fas fa-chart-line"></i> Economic & Global
|
||||||
|
</h3>
|
||||||
<div class="trend-cards">
|
<div class="trend-cards">
|
||||||
<div class="trend-card">
|
<div class="trend-card">
|
||||||
<div class="trend-header">
|
<div class="trend-header">
|
||||||
@ -159,10 +223,18 @@
|
|||||||
<span class="trend-priority high">Transformative</span>
|
<span class="trend-priority high">Transformative</span>
|
||||||
</div>
|
</div>
|
||||||
<h4 class="trend-name">Tokenized Cross-Border Payments</h4>
|
<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>
|
<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">
|
<div class="trend-metrics">
|
||||||
<span class="metric"><i class="fas fa-globe"></i> 75% G20 Adoption</span>
|
<span class="metric"
|
||||||
<span class="metric"><i class="fas fa-exchange-alt"></i> Borderless</span>
|
><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>
|
</div>
|
||||||
|
|
||||||
@ -172,10 +244,18 @@
|
|||||||
<span class="trend-priority medium">Volatile</span>
|
<span class="trend-priority medium">Volatile</span>
|
||||||
</div>
|
</div>
|
||||||
<h4 class="trend-name">Trade Realignments</h4>
|
<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>
|
<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">
|
<div class="trend-metrics">
|
||||||
<span class="metric"><i class="fas fa-balance-scale"></i> Geopolitical Shift</span>
|
<span class="metric"
|
||||||
<span class="metric"><i class="fas fa-industry"></i> Supply Chain Impact</span>
|
><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>
|
</div>
|
||||||
|
|
||||||
@ -185,10 +265,18 @@
|
|||||||
<span class="trend-priority high">Critical</span>
|
<span class="trend-priority high">Critical</span>
|
||||||
</div>
|
</div>
|
||||||
<h4 class="trend-name">Debt Sustainability Challenges</h4>
|
<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>
|
<p class="trend-description">
|
||||||
|
Record public debt levels with limited fiscal restraint
|
||||||
|
appetite as central banks unwind balance sheets.
|
||||||
|
</p>
|
||||||
<div class="trend-metrics">
|
<div class="trend-metrics">
|
||||||
<span class="metric"><i class="fas fa-exclamation-triangle"></i> Record Levels</span>
|
<span class="metric"
|
||||||
<span class="metric"><i class="fas fa-percentage"></i> Yield Pressure</span>
|
><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>
|
||||||
@ -202,7 +290,9 @@
|
|||||||
<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">
|
||||||
|
High-growth markets and strategic investment areas
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="opportunities-grid">
|
<div class="opportunities-grid">
|
||||||
@ -211,7 +301,10 @@
|
|||||||
<i class="fas fa-solar-panel"></i>
|
<i class="fas fa-solar-panel"></i>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="opportunity-title">Climate Technology</h3>
|
<h3 class="opportunity-title">Climate Technology</h3>
|
||||||
<p class="opportunity-description">Home energy solutions, carbon capture, and sustainable infrastructure with massive growth potential.</p>
|
<p class="opportunity-description">
|
||||||
|
Home energy solutions, carbon capture, and sustainable
|
||||||
|
infrastructure with massive growth potential.
|
||||||
|
</p>
|
||||||
<div class="opportunity-market">
|
<div class="opportunity-market">
|
||||||
<span class="market-size">$162B+</span>
|
<span class="market-size">$162B+</span>
|
||||||
<span class="market-label">by 2030</span>
|
<span class="market-label">by 2030</span>
|
||||||
@ -223,7 +316,10 @@
|
|||||||
<i class="fas fa-heartbeat"></i>
|
<i class="fas fa-heartbeat"></i>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="opportunity-title">Preventive Health</h3>
|
<h3 class="opportunity-title">Preventive Health</h3>
|
||||||
<p class="opportunity-description">Personalized wellness, early intervention technologies, and digital health platforms.</p>
|
<p class="opportunity-description">
|
||||||
|
Personalized wellness, early intervention technologies, and
|
||||||
|
digital health platforms.
|
||||||
|
</p>
|
||||||
<div class="opportunity-market">
|
<div class="opportunity-market">
|
||||||
<span class="market-size">High Growth</span>
|
<span class="market-size">High Growth</span>
|
||||||
<span class="market-label">Post-pandemic focus</span>
|
<span class="market-label">Post-pandemic focus</span>
|
||||||
@ -235,7 +331,10 @@
|
|||||||
<i class="fas fa-robot"></i>
|
<i class="fas fa-robot"></i>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="opportunity-title">AI Consulting</h3>
|
<h3 class="opportunity-title">AI Consulting</h3>
|
||||||
<p class="opportunity-description">Industry-specific AI implementation services and agentic AI platform development.</p>
|
<p class="opportunity-description">
|
||||||
|
Industry-specific AI implementation services and agentic AI
|
||||||
|
platform development.
|
||||||
|
</p>
|
||||||
<div class="opportunity-market">
|
<div class="opportunity-market">
|
||||||
<span class="market-size">Specialized</span>
|
<span class="market-size">Specialized</span>
|
||||||
<span class="market-label">Enterprise demand</span>
|
<span class="market-label">Enterprise demand</span>
|
||||||
@ -247,7 +346,10 @@
|
|||||||
<i class="fas fa-seedling"></i>
|
<i class="fas fa-seedling"></i>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="opportunity-title">Plant-Based Foods</h3>
|
<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>
|
<p class="opportunity-description">
|
||||||
|
Sustainable food alternatives with projected market growth toward
|
||||||
|
$162 billion by 2030.
|
||||||
|
</p>
|
||||||
<div class="opportunity-market">
|
<div class="opportunity-market">
|
||||||
<span class="market-size">$162B</span>
|
<span class="market-size">$162B</span>
|
||||||
<span class="market-label">Market potential</span>
|
<span class="market-label">Market potential</span>
|
||||||
@ -258,7 +360,12 @@
|
|||||||
<div class="opportunity-highlight">
|
<div class="opportunity-highlight">
|
||||||
<div class="highlight-content">
|
<div class="highlight-content">
|
||||||
<h3 class="highlight-title">Strategic Investment Shift</h3>
|
<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>
|
<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>
|
||||||
<div class="highlight-stats">
|
<div class="highlight-stats">
|
||||||
<div class="stat-item">
|
<div class="stat-item">
|
||||||
@ -279,7 +386,9 @@
|
|||||||
<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">
|
||||||
|
Navigating complexity in an uncertain landscape
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="challenges-content">
|
<div class="challenges-content">
|
||||||
@ -288,10 +397,16 @@
|
|||||||
<span class="challenge-severity high">High Risk</span>
|
<span class="challenge-severity high">High Risk</span>
|
||||||
<h3 class="challenge-title">AI Security Vulnerabilities</h3>
|
<h3 class="challenge-title">AI Security Vulnerabilities</h3>
|
||||||
</div>
|
</div>
|
||||||
<p class="challenge-description">New attack vectors require comprehensive defense strategies as autonomous agents proliferate across organizations.</p>
|
<p class="challenge-description">
|
||||||
|
New attack vectors require comprehensive defense strategies as
|
||||||
|
autonomous agents proliferate across organizations.
|
||||||
|
</p>
|
||||||
<div class="challenge-mitigation">
|
<div class="challenge-mitigation">
|
||||||
<span class="mitigation-label">Mitigation:</span>
|
<span class="mitigation-label">Mitigation:</span>
|
||||||
<span class="mitigation-text">Robust governance frameworks and AI-native security protocols</span>
|
<span class="mitigation-text"
|
||||||
|
>Robust governance frameworks and AI-native security
|
||||||
|
protocols</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -300,10 +415,16 @@
|
|||||||
<span class="challenge-severity medium">Medium Risk</span>
|
<span class="challenge-severity medium">Medium Risk</span>
|
||||||
<h3 class="challenge-title">Talent & Skills Gap</h3>
|
<h3 class="challenge-title">Talent & Skills Gap</h3>
|
||||||
</div>
|
</div>
|
||||||
<p class="challenge-description">Rapid technological change outpacing workforce skill development, creating critical talent shortages.</p>
|
<p class="challenge-description">
|
||||||
|
Rapid technological change outpacing workforce skill development,
|
||||||
|
creating critical talent shortages.
|
||||||
|
</p>
|
||||||
<div class="challenge-mitigation">
|
<div class="challenge-mitigation">
|
||||||
<span class="mitigation-label">Mitigation:</span>
|
<span class="mitigation-label">Mitigation:</span>
|
||||||
<span class="mitigation-text">Continuous upskilling programs and AI collaboration training</span>
|
<span class="mitigation-text"
|
||||||
|
>Continuous upskilling programs and AI collaboration
|
||||||
|
training</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -312,10 +433,15 @@
|
|||||||
<span class="challenge-severity high">High Risk</span>
|
<span class="challenge-severity high">High Risk</span>
|
||||||
<h3 class="challenge-title">Economic Volatility</h3>
|
<h3 class="challenge-title">Economic Volatility</h3>
|
||||||
</div>
|
</div>
|
||||||
<p class="challenge-description">Potential AI bubble concerns, trade fragmentation, and competing payment systems creating market uncertainty.</p>
|
<p class="challenge-description">
|
||||||
|
Potential AI bubble concerns, trade fragmentation, and competing
|
||||||
|
payment systems creating market uncertainty.
|
||||||
|
</p>
|
||||||
<div class="challenge-mitigation">
|
<div class="challenge-mitigation">
|
||||||
<span class="mitigation-label">Mitigation:</span>
|
<span class="mitigation-label">Mitigation:</span>
|
||||||
<span class="mitigation-text">Diversified portfolios and agile business models</span>
|
<span class="mitigation-text"
|
||||||
|
>Diversified portfolios and agile business models</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -325,15 +451,28 @@
|
|||||||
<div class="implications-grid">
|
<div class="implications-grid">
|
||||||
<div class="implication">
|
<div class="implication">
|
||||||
<h4>For Businesses</h4>
|
<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>
|
<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>
|
||||||
<div class="implication">
|
<div class="implication">
|
||||||
<h4>For Investors</h4>
|
<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>
|
<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>
|
||||||
<div class="implication">
|
<div class="implication">
|
||||||
<h4>For Individuals</h4>
|
<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>
|
<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>
|
||||||
@ -347,7 +486,9 @@
|
|||||||
<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">
|
||||||
|
An analysis of trends shaping the future landscape
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="footer-links">
|
<div class="footer-links">
|
||||||
@ -368,10 +509,17 @@
|
|||||||
|
|
||||||
<div class="footer-bottom">
|
<div class="footer-bottom">
|
||||||
<div class="copyright">
|
<div class="copyright">
|
||||||
<p>© 2026 Horizons Analysis. Based on current research and expert predictions.</p>
|
<p>
|
||||||
|
© 2026 Horizons Analysis. Based on current research and
|
||||||
|
expert predictions.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="deerflow-branding">
|
<div class="deerflow-branding">
|
||||||
<a href="https://deerflow.tech" target="_blank" class="deerflow-link">
|
<a
|
||||||
|
href="https://deerflow.tech"
|
||||||
|
target="_blank"
|
||||||
|
class="deerflow-link"
|
||||||
|
>
|
||||||
<span class="deerflow-icon">✦</span>
|
<span class="deerflow-icon">✦</span>
|
||||||
<span class="deerflow-text">Created by Deerflow</span>
|
<span class="deerflow-text">Created by Deerflow</span>
|
||||||
</a>
|
</a>
|
||||||
@ -381,5 +529,5 @@
|
|||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script src="script.js"></script>
|
<script src="script.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@ -1,103 +1,108 @@
|
|||||||
// 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() {
|
themeToggle.addEventListener("click", function () {
|
||||||
const currentTheme = document.documentElement.getAttribute('data-theme');
|
const currentTheme = document.documentElement.getAttribute("data-theme");
|
||||||
|
|
||||||
if (currentTheme === 'dark') {
|
if (currentTheme === "dark") {
|
||||||
document.documentElement.removeAttribute('data-theme');
|
document.documentElement.removeAttribute("data-theme");
|
||||||
themeIcon.className = 'fas fa-moon';
|
themeIcon.className = "fas fa-moon";
|
||||||
localStorage.setItem('theme', 'light');
|
localStorage.setItem("theme", "light");
|
||||||
} else {
|
} else {
|
||||||
document.documentElement.setAttribute('data-theme', 'dark');
|
document.documentElement.setAttribute("data-theme", "dark");
|
||||||
themeIcon.className = 'fas fa-sun';
|
themeIcon.className = "fas fa-sun";
|
||||||
localStorage.setItem('theme', 'dark');
|
localStorage.setItem("theme", "dark");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Smooth scroll for navigation links
|
// Smooth scroll for navigation links
|
||||||
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 targetId = this.getAttribute('href');
|
const targetId = this.getAttribute("href");
|
||||||
if (targetId === '#') return;
|
if (targetId === "#") return;
|
||||||
|
|
||||||
const targetElement = document.querySelector(targetId);
|
const targetElement = document.querySelector(targetId);
|
||||||
if (targetElement) {
|
if (targetElement) {
|
||||||
const headerHeight = document.querySelector('.navbar').offsetHeight;
|
const headerHeight = document.querySelector(".navbar").offsetHeight;
|
||||||
const targetPosition = targetElement.offsetTop - headerHeight - 20;
|
const targetPosition = targetElement.offsetTop - headerHeight - 20;
|
||||||
|
|
||||||
window.scrollTo({
|
window.scrollTo({
|
||||||
top: targetPosition,
|
top: targetPosition,
|
||||||
behavior: 'smooth'
|
behavior: "smooth",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Navbar scroll effect
|
// Navbar scroll effect
|
||||||
const navbar = document.querySelector('.navbar');
|
const navbar = document.querySelector(".navbar");
|
||||||
let lastScrollTop = 0;
|
let lastScrollTop = 0;
|
||||||
|
|
||||||
window.addEventListener('scroll', function() {
|
window.addEventListener("scroll", function () {
|
||||||
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
||||||
|
|
||||||
// Hide/show navbar on scroll
|
// Hide/show navbar on scroll
|
||||||
if (scrollTop > lastScrollTop && scrollTop > 100) {
|
if (scrollTop > lastScrollTop && scrollTop > 100) {
|
||||||
navbar.style.transform = 'translateY(-100%)';
|
navbar.style.transform = "translateY(-100%)";
|
||||||
} else {
|
} else {
|
||||||
navbar.style.transform = 'translateY(0)';
|
navbar.style.transform = "translateY(0)";
|
||||||
}
|
}
|
||||||
|
|
||||||
lastScrollTop = scrollTop;
|
lastScrollTop = scrollTop;
|
||||||
|
|
||||||
// Add shadow when scrolled
|
// Add shadow when scrolled
|
||||||
if (scrollTop > 10) {
|
if (scrollTop > 10) {
|
||||||
navbar.style.boxShadow = 'var(--shadow-md)';
|
navbar.style.boxShadow = "var(--shadow-md)";
|
||||||
} else {
|
} else {
|
||||||
navbar.style.boxShadow = 'none';
|
navbar.style.boxShadow = "none";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Animate elements on scroll
|
// Animate elements on scroll
|
||||||
const observerOptions = {
|
const observerOptions = {
|
||||||
threshold: 0.1,
|
threshold: 0.1,
|
||||||
rootMargin: '0px 0px -50px 0px'
|
rootMargin: "0px 0px -50px 0px",
|
||||||
};
|
};
|
||||||
|
|
||||||
const observer = new IntersectionObserver(function(entries) {
|
const observer = new IntersectionObserver(function (entries) {
|
||||||
entries.forEach(entry => {
|
entries.forEach((entry) => {
|
||||||
if (entry.isIntersecting) {
|
if (entry.isIntersecting) {
|
||||||
entry.target.classList.add('fade-in');
|
entry.target.classList.add("fade-in");
|
||||||
observer.unobserve(entry.target);
|
observer.unobserve(entry.target);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, observerOptions);
|
}, observerOptions);
|
||||||
|
|
||||||
// Observe elements to animate
|
// Observe elements to animate
|
||||||
document.querySelectorAll('.trend-card, .opportunity-card, .challenge-card, .highlight-card').forEach(el => {
|
document
|
||||||
|
.querySelectorAll(
|
||||||
|
".trend-card, .opportunity-card, .challenge-card, .highlight-card",
|
||||||
|
)
|
||||||
|
.forEach((el) => {
|
||||||
observer.observe(el);
|
observer.observe(el);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Stats counter animation
|
// Stats counter animation
|
||||||
const stats = document.querySelectorAll('.stat-number');
|
const stats = document.querySelectorAll(".stat-number");
|
||||||
|
|
||||||
const statsObserver = new IntersectionObserver(function(entries) {
|
const statsObserver = new IntersectionObserver(
|
||||||
entries.forEach(entry => {
|
function (entries) {
|
||||||
|
entries.forEach((entry) => {
|
||||||
if (entry.isIntersecting) {
|
if (entry.isIntersecting) {
|
||||||
const stat = entry.target;
|
const stat = entry.target;
|
||||||
const targetValue = parseInt(stat.textContent);
|
const targetValue = parseInt(stat.textContent);
|
||||||
@ -119,38 +124,45 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
statsObserver.unobserve(stat);
|
statsObserver.unobserve(stat);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, { threshold: 0.5 });
|
},
|
||||||
|
{ threshold: 0.5 },
|
||||||
|
);
|
||||||
|
|
||||||
stats.forEach(stat => {
|
stats.forEach((stat) => {
|
||||||
statsObserver.observe(stat);
|
statsObserver.observe(stat);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Hover effects for cards
|
// Hover effects for cards
|
||||||
document.querySelectorAll('.trend-card, .opportunity-card, .challenge-card').forEach(card => {
|
document
|
||||||
card.addEventListener('mouseenter', function() {
|
.querySelectorAll(".trend-card, .opportunity-card, .challenge-card")
|
||||||
this.style.zIndex = '10';
|
.forEach((card) => {
|
||||||
|
card.addEventListener("mouseenter", function () {
|
||||||
|
this.style.zIndex = "10";
|
||||||
});
|
});
|
||||||
|
|
||||||
card.addEventListener('mouseleave', function() {
|
card.addEventListener("mouseleave", function () {
|
||||||
this.style.zIndex = '1';
|
this.style.zIndex = "1";
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Current year in footer
|
// Current year in footer
|
||||||
const currentYear = new Date().getFullYear();
|
const currentYear = new Date().getFullYear();
|
||||||
const yearElement = document.querySelector('.copyright p');
|
const yearElement = document.querySelector(".copyright p");
|
||||||
if (yearElement) {
|
if (yearElement) {
|
||||||
yearElement.textContent = yearElement.textContent.replace('2026', currentYear);
|
yearElement.textContent = yearElement.textContent.replace(
|
||||||
|
"2026",
|
||||||
|
currentYear,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize animations
|
// Initialize animations
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
document.body.style.opacity = '1';
|
document.body.style.opacity = "1";
|
||||||
}, 100);
|
}, 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;
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
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
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
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
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
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._
|
||||||
|
|||||||
@ -1,21 +1,24 @@
|
|||||||
<!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>Caren — Pure Skincare</title>
|
<title>Caren — Pure Skincare</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;1,300;1,400&family=Montserrat:wght@300;400;500;600&display=swap" rel="stylesheet">
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;1,300;1,400&family=Montserrat:wght@300;400;500;600&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--blush: #F5E6E0;
|
--blush: #f5e6e0;
|
||||||
--blush-dark: #E8D5CD;
|
--blush-dark: #e8d5cd;
|
||||||
--cream: #FAF7F5;
|
--cream: #faf7f5;
|
||||||
--charcoal: #2C2C2C;
|
--charcoal: #2c2c2c;
|
||||||
--soft-gray: #8A8A8A;
|
--soft-gray: #8a8a8a;
|
||||||
--light-gray: #E5E5E5;
|
--light-gray: #e5e5e5;
|
||||||
--accent: #D4A59A;
|
--accent: #d4a59a;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
@ -29,15 +32,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Montserrat', sans-serif;
|
font-family: "Montserrat", sans-serif;
|
||||||
background: var(--cream);
|
background: var(--cream);
|
||||||
color: var(--charcoal);
|
color: var(--charcoal);
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Typography */
|
/* Typography */
|
||||||
h1, h2, h3, .display {
|
h1,
|
||||||
font-family: 'Cormorant Garamond', serif;
|
h2,
|
||||||
|
h3,
|
||||||
|
.display {
|
||||||
|
font-family: "Cormorant Garamond", serif;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
letter-spacing: -0.02em;
|
letter-spacing: -0.02em;
|
||||||
}
|
}
|
||||||
@ -64,7 +70,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
font-family: 'Cormorant Garamond', serif;
|
font-family: "Cormorant Garamond", serif;
|
||||||
font-size: 1.8rem;
|
font-size: 1.8rem;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
letter-spacing: 0.3em;
|
letter-spacing: 0.3em;
|
||||||
@ -90,7 +96,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.nav-links a::after {
|
.nav-links a::after {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: -4px;
|
bottom: -4px;
|
||||||
left: 0;
|
left: 0;
|
||||||
@ -210,7 +216,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.hero-image::before {
|
.hero-image::before {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -30px;
|
top: -30px;
|
||||||
right: -30px;
|
right: -30px;
|
||||||
@ -246,7 +252,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.marquee-item::after {
|
.marquee-item::after {
|
||||||
content: '✦';
|
content: "✦";
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -357,7 +363,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.product-card::before {
|
.product-card::before {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
@ -374,7 +380,7 @@
|
|||||||
|
|
||||||
.product-card:hover {
|
.product-card:hover {
|
||||||
transform: translateY(-8px);
|
transform: translateY(-8px);
|
||||||
box-shadow: 0 20px 40px rgba(0,0,0,0.05);
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-icon {
|
.product-icon {
|
||||||
@ -482,7 +488,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ingredient-list li::before {
|
.ingredient-list li::before {
|
||||||
content: '✦';
|
content: "✦";
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -505,7 +511,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.testimonial {
|
.testimonial {
|
||||||
font-family: 'Cormorant Garamond', serif;
|
font-family: "Cormorant Garamond", serif;
|
||||||
font-size: clamp(1.5rem, 3vw, 2rem);
|
font-size: clamp(1.5rem, 3vw, 2rem);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
@ -550,7 +556,7 @@
|
|||||||
padding: 1rem 1.5rem;
|
padding: 1rem 1.5rem;
|
||||||
border: 1px solid var(--light-gray);
|
border: 1px solid var(--light-gray);
|
||||||
background: var(--cream);
|
background: var(--cream);
|
||||||
font-family: 'Montserrat', sans-serif;
|
font-family: "Montserrat", sans-serif;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
outline: none;
|
outline: none;
|
||||||
transition: border-color 0.3s ease;
|
transition: border-color 0.3s ease;
|
||||||
@ -667,8 +673,12 @@
|
|||||||
|
|
||||||
/* Animations */
|
/* Animations */
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
from { opacity: 0; }
|
from {
|
||||||
to { opacity: 1; }
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeInUp {
|
@keyframes fadeInUp {
|
||||||
@ -683,8 +693,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes marquee {
|
@keyframes marquee {
|
||||||
0% { transform: translateX(0); }
|
0% {
|
||||||
100% { transform: translateX(-50%); }
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Scroll Reveal */
|
/* Scroll Reveal */
|
||||||
@ -776,8 +790,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Navigation -->
|
<!-- Navigation -->
|
||||||
<nav id="navbar">
|
<nav id="navbar">
|
||||||
<a href="#" class="logo">CAREN</a>
|
<a href="#" class="logo">CAREN</a>
|
||||||
@ -795,16 +809,25 @@
|
|||||||
<div class="hero-content">
|
<div class="hero-content">
|
||||||
<span class="hero-tag">New Collection</span>
|
<span class="hero-tag">New Collection</span>
|
||||||
<h1>Pure Beauty, <em>Simplified</em></h1>
|
<h1>Pure Beauty, <em>Simplified</em></h1>
|
||||||
<p>Discover the art of less. Our minimalist skincare routine delivers maximum results with carefully curated, clean ingredients that honor your skin's natural balance.</p>
|
<p>
|
||||||
|
Discover the art of less. Our minimalist skincare routine delivers
|
||||||
|
maximum results with carefully curated, clean ingredients that honor
|
||||||
|
your skin's natural balance.
|
||||||
|
</p>
|
||||||
<a href="#products" class="hero-cta">
|
<a href="#products" class="hero-cta">
|
||||||
Explore Collection
|
Explore Collection
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
<path d="M5 12h14M12 5l7 7-7 7"/>
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path d="M5 12h14M12 5l7 7-7 7" />
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-image">
|
<div class="hero-image">
|
||||||
<img src="caren-hero.jpg" alt="Caren Skincare Product">
|
<img src="caren-hero.jpg" alt="Caren Skincare Product" />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@ -827,12 +850,20 @@
|
|||||||
<!-- Philosophy Section -->
|
<!-- Philosophy Section -->
|
||||||
<section class="philosophy" id="philosophy">
|
<section class="philosophy" id="philosophy">
|
||||||
<div class="philosophy-image reveal">
|
<div class="philosophy-image reveal">
|
||||||
<img src="caren-lifestyle.jpg" alt="Skincare Ritual">
|
<img src="caren-lifestyle.jpg" alt="Skincare Ritual" />
|
||||||
</div>
|
</div>
|
||||||
<div class="philosophy-content reveal">
|
<div class="philosophy-content reveal">
|
||||||
<h2>Less is <em>More</em></h2>
|
<h2>Less is <em>More</em></h2>
|
||||||
<p>We believe in the power of simplicity. In a world of overwhelming choices, Caren offers a refined selection of essential skincare products that work in harmony with your skin.</p>
|
<p>
|
||||||
<p>Each formula is crafted with intention, using only the finest plant-based ingredients backed by science. No fillers, no fragrances, no compromise.</p>
|
We believe in the power of simplicity. In a world of overwhelming
|
||||||
|
choices, Caren offers a refined selection of essential skincare
|
||||||
|
products that work in harmony with your skin.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Each formula is crafted with intention, using only the finest
|
||||||
|
plant-based ingredients backed by science. No fillers, no fragrances,
|
||||||
|
no compromise.
|
||||||
|
</p>
|
||||||
<div class="stats">
|
<div class="stats">
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<h3>98%</h3>
|
<h3>98%</h3>
|
||||||
@ -861,21 +892,30 @@
|
|||||||
<div class="product-icon">✦</div>
|
<div class="product-icon">✦</div>
|
||||||
<h3>Gentle Cleanser</h3>
|
<h3>Gentle Cleanser</h3>
|
||||||
<div class="price">$38</div>
|
<div class="price">$38</div>
|
||||||
<p>A soft, cloud-like formula that removes impurities without stripping your skin's natural moisture barrier.</p>
|
<p>
|
||||||
|
A soft, cloud-like formula that removes impurities without stripping
|
||||||
|
your skin's natural moisture barrier.
|
||||||
|
</p>
|
||||||
<button class="product-btn">Add to Cart</button>
|
<button class="product-btn">Add to Cart</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="product-card reveal">
|
<div class="product-card reveal">
|
||||||
<div class="product-icon">◈</div>
|
<div class="product-icon">◈</div>
|
||||||
<h3>Hydrating Serum</h3>
|
<h3>Hydrating Serum</h3>
|
||||||
<div class="price">$68</div>
|
<div class="price">$68</div>
|
||||||
<p>Deep hydration with hyaluronic acid and vitamin B5 for plump, radiant skin that glows from within.</p>
|
<p>
|
||||||
|
Deep hydration with hyaluronic acid and vitamin B5 for plump,
|
||||||
|
radiant skin that glows from within.
|
||||||
|
</p>
|
||||||
<button class="product-btn">Add to Cart</button>
|
<button class="product-btn">Add to Cart</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="product-card reveal">
|
<div class="product-card reveal">
|
||||||
<div class="product-icon">✧</div>
|
<div class="product-icon">✧</div>
|
||||||
<h3>Repair Moisturizer</h3>
|
<h3>Repair Moisturizer</h3>
|
||||||
<div class="price">$58</div>
|
<div class="price">$58</div>
|
||||||
<p>Rich yet lightweight, this moisturizer locks in hydration while supporting your skin's natural repair process.</p>
|
<p>
|
||||||
|
Rich yet lightweight, this moisturizer locks in hydration while
|
||||||
|
supporting your skin's natural repair process.
|
||||||
|
</p>
|
||||||
<button class="product-btn">Add to Cart</button>
|
<button class="product-btn">Add to Cart</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -885,7 +925,11 @@
|
|||||||
<section class="ingredients" id="ingredients">
|
<section class="ingredients" id="ingredients">
|
||||||
<div class="ingredients-content reveal">
|
<div class="ingredients-content reveal">
|
||||||
<h2>Ingredients You Can <em>Trust</em></h2>
|
<h2>Ingredients You Can <em>Trust</em></h2>
|
||||||
<p>Transparency is at the heart of everything we do. Every ingredient serves a purpose, carefully selected for its proven efficacy and skin-loving properties.</p>
|
<p>
|
||||||
|
Transparency is at the heart of everything we do. Every ingredient
|
||||||
|
serves a purpose, carefully selected for its proven efficacy and
|
||||||
|
skin-loving properties.
|
||||||
|
</p>
|
||||||
<ul class="ingredient-list">
|
<ul class="ingredient-list">
|
||||||
<li>Hyaluronic Acid — Deep hydration</li>
|
<li>Hyaluronic Acid — Deep hydration</li>
|
||||||
<li>Niacinamide — Brightening & pore refining</li>
|
<li>Niacinamide — Brightening & pore refining</li>
|
||||||
@ -895,7 +939,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="ingredients-image reveal">
|
<div class="ingredients-image reveal">
|
||||||
<img src="caren-ingredients.jpg" alt="Natural Ingredients">
|
<img src="caren-ingredients.jpg" alt="Natural Ingredients" />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@ -903,7 +947,11 @@
|
|||||||
<section class="testimonials">
|
<section class="testimonials">
|
||||||
<h2 class="reveal">Words from Our Community</h2>
|
<h2 class="reveal">Words from Our Community</h2>
|
||||||
<div class="testimonial-slider reveal">
|
<div class="testimonial-slider reveal">
|
||||||
<p class="testimonial">"Finally, a skincare brand that understands simplicity. My skin has never looked better, and my routine has never been simpler. Caren is pure magic."</p>
|
<p class="testimonial">
|
||||||
|
"Finally, a skincare brand that understands simplicity. My skin has
|
||||||
|
never looked better, and my routine has never been simpler. Caren is
|
||||||
|
pure magic."
|
||||||
|
</p>
|
||||||
<span class="testimonial-author">— Sarah M., Verified Buyer</span>
|
<span class="testimonial-author">— Sarah M., Verified Buyer</span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -911,9 +959,12 @@
|
|||||||
<!-- Newsletter -->
|
<!-- Newsletter -->
|
||||||
<section class="newsletter">
|
<section class="newsletter">
|
||||||
<h2 class="reveal">Join the Caren Family</h2>
|
<h2 class="reveal">Join the Caren Family</h2>
|
||||||
<p class="reveal">Subscribe for exclusive offers, skincare tips, and early access to new releases.</p>
|
<p class="reveal">
|
||||||
|
Subscribe for exclusive offers, skincare tips, and early access to new
|
||||||
|
releases.
|
||||||
|
</p>
|
||||||
<form class="newsletter-form reveal">
|
<form class="newsletter-form reveal">
|
||||||
<input type="email" placeholder="Your email address" required>
|
<input type="email" placeholder="Your email address" required />
|
||||||
<button type="submit">Subscribe</button>
|
<button type="submit">Subscribe</button>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
@ -923,7 +974,10 @@
|
|||||||
<div class="footer-content">
|
<div class="footer-content">
|
||||||
<div class="footer-brand">
|
<div class="footer-brand">
|
||||||
<a href="#" class="logo">CAREN</a>
|
<a href="#" class="logo">CAREN</a>
|
||||||
<p>Pure, minimalist skincare for the modern individual. Made with intention, delivered with care.</p>
|
<p>
|
||||||
|
Pure, minimalist skincare for the modern individual. Made with
|
||||||
|
intention, delivered with care.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer-col">
|
<div class="footer-col">
|
||||||
<h4>Shop</h4>
|
<h4>Shop</h4>
|
||||||
@ -960,70 +1014,79 @@
|
|||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<!-- Deerflow Badge -->
|
<!-- Deerflow Badge -->
|
||||||
<a href="https://deerflow.tech" target="_blank" class="deerflow-badge">Created By Deerflow</a>
|
<a href="https://deerflow.tech" target="_blank" class="deerflow-badge"
|
||||||
|
>Created By Deerflow</a
|
||||||
|
>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Navbar scroll effect
|
// Navbar scroll effect
|
||||||
const navbar = document.getElementById('navbar');
|
const navbar = document.getElementById("navbar");
|
||||||
window.addEventListener('scroll', () => {
|
window.addEventListener("scroll", () => {
|
||||||
if (window.scrollY > 50) {
|
if (window.scrollY > 50) {
|
||||||
navbar.classList.add('scrolled');
|
navbar.classList.add("scrolled");
|
||||||
} else {
|
} else {
|
||||||
navbar.classList.remove('scrolled');
|
navbar.classList.remove("scrolled");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Scroll reveal animation
|
// Scroll reveal animation
|
||||||
const revealElements = document.querySelectorAll('.reveal');
|
const revealElements = document.querySelectorAll(".reveal");
|
||||||
|
|
||||||
const revealObserver = new IntersectionObserver((entries) => {
|
const revealObserver = new IntersectionObserver(
|
||||||
entries.forEach(entry => {
|
(entries) => {
|
||||||
|
entries.forEach((entry) => {
|
||||||
if (entry.isIntersecting) {
|
if (entry.isIntersecting) {
|
||||||
entry.target.classList.add('active');
|
entry.target.classList.add("active");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
threshold: 0.1,
|
threshold: 0.1,
|
||||||
rootMargin: '0px 0px -50px 0px'
|
rootMargin: "0px 0px -50px 0px",
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
revealElements.forEach(el => revealObserver.observe(el));
|
revealElements.forEach((el) => revealObserver.observe(el));
|
||||||
|
|
||||||
// Smooth scroll for anchor links
|
// Smooth scroll for anchor links
|
||||||
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) {
|
||||||
target.scrollIntoView({
|
target.scrollIntoView({
|
||||||
behavior: 'smooth',
|
behavior: "smooth",
|
||||||
block: 'start'
|
block: "start",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Newsletter form
|
// Newsletter form
|
||||||
document.querySelector('.newsletter-form').addEventListener('submit', (e) => {
|
document
|
||||||
|
.querySelector(".newsletter-form")
|
||||||
|
.addEventListener("submit", (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const input = e.target.querySelector('input');
|
const input = e.target.querySelector("input");
|
||||||
if (input.value) {
|
if (input.value) {
|
||||||
alert('Thank you for subscribing! Welcome to the Caren family.');
|
alert("Thank you for subscribing! Welcome to the Caren family.");
|
||||||
input.value = '';
|
input.value = "";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Product buttons
|
// Product buttons
|
||||||
document.querySelectorAll('.product-btn').forEach(btn => {
|
document.querySelectorAll(".product-btn").forEach((btn) => {
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener("click", () => {
|
||||||
alert('Added to cart! This is a demo landing page.');
|
alert("Added to cart! This is a demo landing page.");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Nav CTA
|
// Nav CTA
|
||||||
document.querySelector('.nav-cta').addEventListener('click', () => {
|
document.querySelector(".nav-cta").addEventListener("click", () => {
|
||||||
document.getElementById('products').scrollIntoView({ behavior: 'smooth' });
|
document
|
||||||
|
.getElementById("products")
|
||||||
|
.scrollIntoView({ behavior: "smooth" });
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -1,15 +1,18 @@
|
|||||||
<!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>
|
||||||
@ -43,8 +46,14 @@
|
|||||||
<p class="hero-tagline">"It is a truth universally acknowledged..."</p>
|
<p class="hero-tagline">"It is a truth universally acknowledged..."</p>
|
||||||
<a href="#about" class="hero-cta">
|
<a href="#about" class="hero-cta">
|
||||||
<span>Discover the Story</span>
|
<span>Discover the Story</span>
|
||||||
<svg class="cta-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
<svg
|
||||||
<path d="M12 5v14M5 12l7 7 7-7"/>
|
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>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@ -62,9 +71,22 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="about-content">
|
<div class="about-content">
|
||||||
<div class="about-text">
|
<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 class="about-lead">
|
||||||
<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>
|
Set in rural England in the early 19th century,
|
||||||
<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>
|
<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>
|
||||||
<div class="about-stats">
|
<div class="about-stats">
|
||||||
<div class="stat-item">
|
<div class="stat-item">
|
||||||
@ -97,7 +119,10 @@
|
|||||||
<div class="character-info">
|
<div class="character-info">
|
||||||
<h3>Elizabeth Bennet</h3>
|
<h3>Elizabeth Bennet</h3>
|
||||||
<p class="character-role">The Protagonist</p>
|
<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>
|
<p class="character-desc">
|
||||||
|
Intelligent, witty, and independent, Elizabeth navigates
|
||||||
|
society's expectations while staying true to her principles.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="character-card featured">
|
<div class="character-card featured">
|
||||||
@ -105,7 +130,10 @@
|
|||||||
<div class="character-info">
|
<div class="character-info">
|
||||||
<h3>Fitzwilliam Darcy</h3>
|
<h3>Fitzwilliam Darcy</h3>
|
||||||
<p class="character-role">The Romantic Lead</p>
|
<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>
|
<p class="character-desc">
|
||||||
|
Wealthy, reserved, and initially perceived as arrogant, Darcy's
|
||||||
|
true character is revealed through his actions.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="character-card">
|
<div class="character-card">
|
||||||
@ -113,7 +141,9 @@
|
|||||||
<div class="character-info">
|
<div class="character-info">
|
||||||
<h3>Jane Bennet</h3>
|
<h3>Jane Bennet</h3>
|
||||||
<p class="character-role">The Eldest Sister</p>
|
<p class="character-role">The Eldest Sister</p>
|
||||||
<p class="character-desc">Beautiful, gentle, and always sees the best in people.</p>
|
<p class="character-desc">
|
||||||
|
Beautiful, gentle, and always sees the best in people.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="character-card">
|
<div class="character-card">
|
||||||
@ -121,7 +151,9 @@
|
|||||||
<div class="character-info">
|
<div class="character-info">
|
||||||
<h3>Charles Bingley</h3>
|
<h3>Charles Bingley</h3>
|
||||||
<p class="character-role">The Amiable Gentleman</p>
|
<p class="character-role">The Amiable Gentleman</p>
|
||||||
<p class="character-desc">Wealthy, good-natured, and easily influenced by his friends.</p>
|
<p class="character-desc">
|
||||||
|
Wealthy, good-natured, and easily influenced by his friends.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="character-card">
|
<div class="character-card">
|
||||||
@ -129,7 +161,9 @@
|
|||||||
<div class="character-info">
|
<div class="character-info">
|
||||||
<h3>Lydia Bennet</h3>
|
<h3>Lydia Bennet</h3>
|
||||||
<p class="character-role">The Youngest Sister</p>
|
<p class="character-role">The Youngest Sister</p>
|
||||||
<p class="character-desc">Frivolous, flirtatious, and impulsive, causing family scandal.</p>
|
<p class="character-desc">
|
||||||
|
Frivolous, flirtatious, and impulsive, causing family scandal.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="character-card">
|
<div class="character-card">
|
||||||
@ -137,7 +171,9 @@
|
|||||||
<div class="character-info">
|
<div class="character-info">
|
||||||
<h3>George Wickham</h3>
|
<h3>George Wickham</h3>
|
||||||
<p class="character-role">The Antagonist</p>
|
<p class="character-role">The Antagonist</p>
|
||||||
<p class="character-desc">Charming on the surface but deceitful and manipulative.</p>
|
<p class="character-desc">
|
||||||
|
Charming on the surface but deceitful and manipulative.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -154,42 +190,84 @@
|
|||||||
<div class="themes-content">
|
<div class="themes-content">
|
||||||
<div class="theme-item">
|
<div class="theme-item">
|
||||||
<div class="theme-icon">
|
<div class="theme-icon">
|
||||||
<svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.5">
|
<svg
|
||||||
<path d="M24 44c11.046 0 20-8.954 20-20S35.046 4 24 4 4 12.954 4 24s8.954 20 20 20z"/>
|
viewBox="0 0 48 48"
|
||||||
<path d="M24 12v20M16 24h16"/>
|
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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h3>Pride</h3>
|
<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>
|
<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>
|
||||||
<div class="theme-item">
|
<div class="theme-item">
|
||||||
<div class="theme-icon">
|
<div class="theme-icon">
|
||||||
<svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.5">
|
<svg
|
||||||
<path d="M24 4l6 12 13 2-9 9 2 13-12-6-12 6 2-13-9-9 13-2z"/>
|
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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h3>Prejudice</h3>
|
<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>
|
<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>
|
||||||
<div class="theme-item">
|
<div class="theme-item">
|
||||||
<div class="theme-icon">
|
<div class="theme-icon">
|
||||||
<svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.5">
|
<svg
|
||||||
<path d="M24 44c11.046 0 20-8.954 20-20S35.046 4 24 4 4 12.954 4 24s8.954 20 20 20z"/>
|
viewBox="0 0 48 48"
|
||||||
<path d="M14 24c0-5.523 4.477-10 10-10s10 4.477 10 10-4.477 10-10 10"/>
|
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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h3>Marriage</h3>
|
<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>
|
<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>
|
||||||
<div class="theme-item">
|
<div class="theme-item">
|
||||||
<div class="theme-icon">
|
<div class="theme-icon">
|
||||||
<svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.5">
|
<svg
|
||||||
<path d="M12 12h24v24H12z"/>
|
viewBox="0 0 48 48"
|
||||||
<path d="M12 12l24 24M36 12L12 36"/>
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
>
|
||||||
|
<path d="M12 12h24v24H12z" />
|
||||||
|
<path d="M12 12l24 24M36 12L12 36" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h3>Class</h3>
|
<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>
|
<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>
|
</div>
|
||||||
@ -205,17 +283,25 @@
|
|||||||
<div class="quotes-slider">
|
<div class="quotes-slider">
|
||||||
<div class="quote-card active">
|
<div class="quote-card active">
|
||||||
<span class="quote-mark">"</span>
|
<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>
|
<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>
|
<cite>— Opening Line</cite>
|
||||||
</div>
|
</div>
|
||||||
<div class="quote-card">
|
<div class="quote-card">
|
||||||
<span class="quote-mark">"</span>
|
<span class="quote-mark">"</span>
|
||||||
<blockquote>I could easily forgive his pride, if he had not mortified mine.</blockquote>
|
<blockquote>
|
||||||
|
I could easily forgive his pride, if he had not mortified mine.
|
||||||
|
</blockquote>
|
||||||
<cite>— Elizabeth Bennet</cite>
|
<cite>— Elizabeth Bennet</cite>
|
||||||
</div>
|
</div>
|
||||||
<div class="quote-card">
|
<div class="quote-card">
|
||||||
<span class="quote-mark">"</span>
|
<span class="quote-mark">"</span>
|
||||||
<blockquote>You have bewitched me, body and soul, and I love, I love, I love you.</blockquote>
|
<blockquote>
|
||||||
|
You have bewitched me, body and soul, and I love, I love, I love
|
||||||
|
you.
|
||||||
|
</blockquote>
|
||||||
<cite>— Mr. Darcy</cite>
|
<cite>— Mr. Darcy</cite>
|
||||||
</div>
|
</div>
|
||||||
<div class="quote-card">
|
<div class="quote-card">
|
||||||
@ -253,12 +339,16 @@
|
|||||||
<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>
|
</div>
|
||||||
<a href="https://deerflow.tech" target="_blank" class="deerflow-signature">
|
<a
|
||||||
|
href="https://deerflow.tech"
|
||||||
|
target="_blank"
|
||||||
|
class="deerflow-signature"
|
||||||
|
>
|
||||||
<span class="signature-text">Created By Deerflow</span>
|
<span class="signature-text">Created By Deerflow</span>
|
||||||
<span class="signature-icon">✦</span>
|
<span class="signature-icon">✦</span>
|
||||||
</a>
|
</a>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script src="script.js"></script>
|
<script src="script.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// Pride and Prejudice - Interactive Features
|
// Pride and Prejudice - Interactive Features
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
// Navigation scroll effect
|
// Navigation scroll effect
|
||||||
initNavigation();
|
initNavigation();
|
||||||
|
|
||||||
@ -18,17 +18,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
// 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;
|
||||||
@ -39,19 +39,19 @@ function initNavigation() {
|
|||||||
// 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;
|
||||||
}
|
}
|
||||||
@ -63,7 +63,7 @@ function initQuotesSlider() {
|
|||||||
|
|
||||||
// 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();
|
||||||
});
|
});
|
||||||
@ -83,9 +83,9 @@ function initQuotesSlider() {
|
|||||||
startAutoSlide();
|
startAutoSlide();
|
||||||
|
|
||||||
// Pause on hover
|
// Pause on hover
|
||||||
const slider = document.querySelector('.quotes-slider');
|
const slider = document.querySelector(".quotes-slider");
|
||||||
slider.addEventListener('mouseenter', () => clearInterval(autoSlideInterval));
|
slider.addEventListener("mouseenter", () => clearInterval(autoSlideInterval));
|
||||||
slider.addEventListener('mouseleave', startAutoSlide);
|
slider.addEventListener("mouseleave", startAutoSlide);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
@ -93,27 +93,28 @@ function initQuotesSlider() {
|
|||||||
// ============================================
|
// ============================================
|
||||||
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") ||
|
||||||
|
entry.target.classList.contains("theme-item")
|
||||||
? index * 100
|
? index * 100
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
entry.target.classList.add('reveal');
|
entry.target.classList.add("reveal");
|
||||||
entry.target.style.opacity = '1';
|
entry.target.style.opacity = "1";
|
||||||
entry.target.style.transform = 'translateY(0)';
|
entry.target.style.transform = "translateY(0)";
|
||||||
}, delay);
|
}, delay);
|
||||||
|
|
||||||
revealObserver.unobserve(entry.target);
|
revealObserver.unobserve(entry.target);
|
||||||
@ -121,10 +122,11 @@ function initScrollReveal() {
|
|||||||
});
|
});
|
||||||
}, revealOptions);
|
}, revealOptions);
|
||||||
|
|
||||||
revealElements.forEach(el => {
|
revealElements.forEach((el) => {
|
||||||
el.style.opacity = '0';
|
el.style.opacity = "0";
|
||||||
el.style.transform = 'translateY(30px)';
|
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)';
|
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);
|
revealObserver.observe(el);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -133,18 +135,19 @@ function initScrollReveal() {
|
|||||||
// 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({
|
window.scrollTo({
|
||||||
top: targetPosition,
|
top: targetPosition,
|
||||||
behavior: 'smooth'
|
behavior: "smooth",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -154,9 +157,9 @@ function initSmoothScroll() {
|
|||||||
// ============================================
|
// ============================================
|
||||||
// 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)`;
|
||||||
@ -166,12 +169,12 @@ window.addEventListener('scroll', () => {
|
|||||||
// ============================================
|
// ============================================
|
||||||
// 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";
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -5,21 +5,21 @@
|
|||||||
/* CSS Variables */
|
/* CSS Variables */
|
||||||
:root {
|
:root {
|
||||||
/* Colors - Regency Era Palette */
|
/* Colors - Regency Era Palette */
|
||||||
--color-cream: #FAF7F2;
|
--color-cream: #faf7f2;
|
||||||
--color-ivory: #F5F0E8;
|
--color-ivory: #f5f0e8;
|
||||||
--color-parchment: #EDE6D6;
|
--color-parchment: #ede6d6;
|
||||||
--color-gold: #C9A962;
|
--color-gold: #c9a962;
|
||||||
--color-gold-light: #D4BC7E;
|
--color-gold-light: #d4bc7e;
|
||||||
--color-burgundy: #722F37;
|
--color-burgundy: #722f37;
|
||||||
--color-burgundy-dark: #5A252C;
|
--color-burgundy-dark: #5a252c;
|
||||||
--color-charcoal: #2C2C2C;
|
--color-charcoal: #2c2c2c;
|
||||||
--color-charcoal-light: #4A4A4A;
|
--color-charcoal-light: #4a4a4a;
|
||||||
--color-sage: #7D8471;
|
--color-sage: #7d8471;
|
||||||
--color-rose: #C4A4A4;
|
--color-rose: #c4a4a4;
|
||||||
|
|
||||||
/* Typography */
|
/* Typography */
|
||||||
--font-display: 'Playfair Display', Georgia, serif;
|
--font-display: "Playfair Display", Georgia, serif;
|
||||||
--font-body: 'Cormorant Garamond', Georgia, serif;
|
--font-body: "Cormorant Garamond", Georgia, serif;
|
||||||
|
|
||||||
/* Spacing */
|
/* Spacing */
|
||||||
--section-padding: 8rem;
|
--section-padding: 8rem;
|
||||||
@ -31,7 +31,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Reset & Base */
|
/* Reset & Base */
|
||||||
*, *::before, *::after {
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@ -70,7 +72,11 @@ body {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 1.5rem 3rem;
|
padding: 1.5rem 3rem;
|
||||||
background: linear-gradient(to bottom, rgba(250, 247, 242, 0.95), transparent);
|
background: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
rgba(250, 247, 242, 0.95),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
transition: var(--transition-quick);
|
transition: var(--transition-quick);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,7 +113,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.nav-links a::after {
|
.nav-links a::after {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
@ -136,7 +142,12 @@ body {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: linear-gradient(135deg, var(--color-cream) 0%, var(--color-ivory) 50%, var(--color-parchment) 100%);
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
var(--color-cream) 0%,
|
||||||
|
var(--color-ivory) 50%,
|
||||||
|
var(--color-parchment) 100%
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-bg {
|
.hero-bg {
|
||||||
@ -149,15 +160,32 @@ body {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
inset: -50%;
|
inset: -50%;
|
||||||
background-image:
|
background-image:
|
||||||
radial-gradient(circle at 20% 30%, rgba(201, 169, 98, 0.08) 0%, transparent 50%),
|
radial-gradient(
|
||||||
radial-gradient(circle at 80% 70%, rgba(114, 47, 55, 0.05) 0%, transparent 50%),
|
circle at 20% 30%,
|
||||||
radial-gradient(circle at 50% 50%, rgba(125, 132, 113, 0.03) 0%, transparent 60%);
|
rgba(201, 169, 98, 0.08) 0%,
|
||||||
|
transparent 50%
|
||||||
|
),
|
||||||
|
radial-gradient(
|
||||||
|
circle at 80% 70%,
|
||||||
|
rgba(114, 47, 55, 0.05) 0%,
|
||||||
|
transparent 50%
|
||||||
|
),
|
||||||
|
radial-gradient(
|
||||||
|
circle at 50% 50%,
|
||||||
|
rgba(125, 132, 113, 0.03) 0%,
|
||||||
|
transparent 60%
|
||||||
|
);
|
||||||
animation: patternFloat 20s ease-in-out infinite;
|
animation: patternFloat 20s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes patternFloat {
|
@keyframes patternFloat {
|
||||||
0%, 100% { transform: translate(0, 0) rotate(0deg); }
|
0%,
|
||||||
50% { transform: translate(2%, 2%) rotate(2deg); }
|
100% {
|
||||||
|
transform: translate(0, 0) rotate(0deg);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translate(2%, 2%) rotate(2deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-content {
|
.hero-content {
|
||||||
@ -257,7 +285,12 @@ body {
|
|||||||
.divider-line {
|
.divider-line {
|
||||||
width: 60px;
|
width: 60px;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
background: linear-gradient(90deg, transparent, var(--color-gold), transparent);
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
transparent,
|
||||||
|
var(--color-gold),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
.divider-ornament {
|
.divider-ornament {
|
||||||
@ -325,8 +358,15 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes scrollPulse {
|
@keyframes scrollPulse {
|
||||||
0%, 100% { opacity: 0.3; transform: scaleY(0.8); }
|
0%,
|
||||||
50% { opacity: 1; transform: scaleY(1); }
|
100% {
|
||||||
|
opacity: 0.3;
|
||||||
|
transform: scaleY(0.8);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scaleY(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeInUp {
|
@keyframes fadeInUp {
|
||||||
@ -341,8 +381,12 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
from { opacity: 0; }
|
from {
|
||||||
to { opacity: 1; }
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
@ -446,7 +490,11 @@ body {
|
|||||||
============================================ */
|
============================================ */
|
||||||
.characters {
|
.characters {
|
||||||
padding: var(--section-padding) 0;
|
padding: var(--section-padding) 0;
|
||||||
background: linear-gradient(to bottom, var(--color-ivory), var(--color-cream));
|
background: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
var(--color-ivory),
|
||||||
|
var(--color-cream)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
.characters-grid {
|
.characters-grid {
|
||||||
@ -474,20 +522,28 @@ body {
|
|||||||
|
|
||||||
.character-portrait {
|
.character-portrait {
|
||||||
height: 200px;
|
height: 200px;
|
||||||
background: linear-gradient(135deg, var(--color-parchment) 0%, var(--color-ivory) 100%);
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
var(--color-parchment) 0%,
|
||||||
|
var(--color-ivory) 100%
|
||||||
|
);
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.character-portrait::before {
|
.character-portrait::before {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background: radial-gradient(circle at 30% 30%, rgba(201, 169, 98, 0.15) 0%, transparent 60%);
|
background: radial-gradient(
|
||||||
|
circle at 30% 30%,
|
||||||
|
rgba(201, 169, 98, 0.15) 0%,
|
||||||
|
transparent 60%
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
.character-portrait.elizabeth::after {
|
.character-portrait.elizabeth::after {
|
||||||
content: '👒';
|
content: "👒";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@ -497,7 +553,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.character-portrait.darcy::after {
|
.character-portrait.darcy::after {
|
||||||
content: '🎩';
|
content: "🎩";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@ -507,7 +563,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.character-portrait.jane::after {
|
.character-portrait.jane::after {
|
||||||
content: '🌸';
|
content: "🌸";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@ -517,7 +573,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.character-portrait.bingley::after {
|
.character-portrait.bingley::after {
|
||||||
content: '🎭';
|
content: "🎭";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@ -527,7 +583,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.character-portrait.lydia::after {
|
.character-portrait.lydia::after {
|
||||||
content: '💃';
|
content: "💃";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@ -537,7 +593,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.character-portrait.wickham::after {
|
.character-portrait.wickham::after {
|
||||||
content: '🎪';
|
content: "🎪";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@ -641,13 +697,17 @@ body {
|
|||||||
============================================ */
|
============================================ */
|
||||||
.quotes {
|
.quotes {
|
||||||
padding: var(--section-padding) 0;
|
padding: var(--section-padding) 0;
|
||||||
background: linear-gradient(135deg, var(--color-parchment) 0%, var(--color-ivory) 100%);
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
var(--color-parchment) 0%,
|
||||||
|
var(--color-ivory) 100%
|
||||||
|
);
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quotes::before {
|
.quotes::before {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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_
|
||||||
|
|||||||
@ -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": [],
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
<!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>Liquid Glass Weather</title>
|
<title>Liquid Glass Weather</title>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Syncopate:wght@400;700&display=swap" rel="stylesheet">
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Syncopate:wght@400;700&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@ -28,8 +31,13 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-family: 'Space Grotesk', sans-serif;
|
font-family: "Space Grotesk", sans-serif;
|
||||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
#1a1a2e 0%,
|
||||||
|
#16213e 50%,
|
||||||
|
#0f3460 100%
|
||||||
|
);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
@ -55,10 +63,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes float {
|
@keyframes float {
|
||||||
0%, 100% { transform: translateY(100vh) rotate(0deg); opacity: 0; }
|
0%,
|
||||||
10% { opacity: 1; }
|
100% {
|
||||||
90% { opacity: 1; }
|
transform: translateY(100vh) rotate(0deg);
|
||||||
100% { transform: translateY(-100vh) rotate(720deg); opacity: 0; }
|
opacity: 0;
|
||||||
|
}
|
||||||
|
10% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
90% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(-100vh) rotate(720deg);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Weather effects container */
|
/* Weather effects container */
|
||||||
@ -80,7 +99,11 @@
|
|||||||
right: -30%;
|
right: -30%;
|
||||||
width: 150%;
|
width: 150%;
|
||||||
height: 150%;
|
height: 150%;
|
||||||
background: radial-gradient(ellipse at center, rgba(255, 200, 100, 0.15) 0%, transparent 60%);
|
background: radial-gradient(
|
||||||
|
ellipse at center,
|
||||||
|
rgba(255, 200, 100, 0.15) 0%,
|
||||||
|
transparent 60%
|
||||||
|
);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 1s ease;
|
transition: opacity 1s ease;
|
||||||
animation: sunPulse 8s infinite ease-in-out;
|
animation: sunPulse 8s infinite ease-in-out;
|
||||||
@ -91,8 +114,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes sunPulse {
|
@keyframes sunPulse {
|
||||||
0%, 100% { transform: scale(1) rotate(0deg); }
|
0%,
|
||||||
50% { transform: scale(1.1) rotate(5deg); }
|
100% {
|
||||||
|
transform: scale(1) rotate(0deg);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.1) rotate(5deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Rain drops */
|
/* Rain drops */
|
||||||
@ -100,7 +128,11 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
width: 2px;
|
width: 2px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
background: linear-gradient(to bottom, transparent, rgba(174, 194, 224, 0.6));
|
background: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
transparent,
|
||||||
|
rgba(174, 194, 224, 0.6)
|
||||||
|
);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
animation: rainFall 1s linear infinite;
|
animation: rainFall 1s linear infinite;
|
||||||
}
|
}
|
||||||
@ -110,8 +142,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes rainFall {
|
@keyframes rainFall {
|
||||||
0% { transform: translateY(-100px); }
|
0% {
|
||||||
100% { transform: translateY(100vh); }
|
transform: translateY(-100px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(100vh);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Snow flakes */
|
/* Snow flakes */
|
||||||
@ -130,8 +166,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes snowFall {
|
@keyframes snowFall {
|
||||||
0% { transform: translateY(-100px) rotate(0deg); }
|
0% {
|
||||||
100% { transform: translateY(100vh) rotate(360deg); }
|
transform: translateY(-100px) rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(100vh) rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Main container */
|
/* Main container */
|
||||||
@ -159,14 +199,16 @@
|
|||||||
inset 0 1px 1px var(--glass-highlight),
|
inset 0 1px 1px var(--glass-highlight),
|
||||||
inset 0 -1px 1px rgba(0, 0, 0, 0.1);
|
inset 0 -1px 1px rgba(0, 0, 0, 0.1);
|
||||||
transform-style: preserve-3d;
|
transform-style: preserve-3d;
|
||||||
transition: transform 0.1s ease-out, box-shadow 0.3s ease;
|
transition:
|
||||||
|
transform 0.1s ease-out,
|
||||||
|
box-shadow 0.3s ease;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Liquid shine effect */
|
/* Liquid shine effect */
|
||||||
.weather-card::before {
|
.weather-card::before {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -50%;
|
top: -50%;
|
||||||
left: -50%;
|
left: -50%;
|
||||||
@ -183,17 +225,26 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes liquidShine {
|
@keyframes liquidShine {
|
||||||
0% { transform: translateX(-100%) translateY(-100%) rotate(45deg); }
|
0% {
|
||||||
100% { transform: translateX(100%) translateY(100%) rotate(45deg); }
|
transform: translateX(-100%) translateY(-100%) rotate(45deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(100%) translateY(100%) rotate(45deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Inner glow */
|
/* Inner glow */
|
||||||
.weather-card::after {
|
.weather-card::after {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 1px;
|
inset: 1px;
|
||||||
border-radius: 29px;
|
border-radius: 29px;
|
||||||
background: linear-gradient(135deg, rgba(255,255,255,0.1) 0%, transparent 50%, rgba(255,255,255,0.05) 100%);
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(255, 255, 255, 0.1) 0%,
|
||||||
|
transparent 50%,
|
||||||
|
rgba(255, 255, 255, 0.05) 100%
|
||||||
|
);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
}
|
}
|
||||||
@ -230,7 +281,7 @@
|
|||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
width: 70px;
|
width: 70px;
|
||||||
height: 70px;
|
height: 70px;
|
||||||
background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
|
background: linear-gradient(135deg, #ffd700 0%, #ffa500 100%);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 0 40px rgba(255, 215, 0, 0.6),
|
0 0 40px rgba(255, 215, 0, 0.6),
|
||||||
@ -250,14 +301,19 @@
|
|||||||
|
|
||||||
.sun-rays-icon::before,
|
.sun-rays-icon::before,
|
||||||
.sun-rays-icon::after {
|
.sun-rays-icon::after {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 4px;
|
height: 4px;
|
||||||
background: linear-gradient(90deg, transparent, rgba(255, 215, 0, 0.8), transparent);
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
transparent,
|
||||||
|
rgba(255, 215, 0, 0.8),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sun-rays-icon::after {
|
.sun-rays-icon::after {
|
||||||
@ -270,13 +326,22 @@
|
|||||||
left: 50%;
|
left: 50%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 4px;
|
height: 4px;
|
||||||
background: linear-gradient(90deg, transparent, rgba(255, 215, 0, 0.6), transparent);
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
transparent,
|
||||||
|
rgba(255, 215, 0, 0.6),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
transform-origin: center;
|
transform-origin: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes sunRotate {
|
@keyframes sunRotate {
|
||||||
0% { transform: translate(-50%, -50%) rotate(0deg); }
|
0% {
|
||||||
100% { transform: translate(-50%, -50%) rotate(360deg); }
|
transform: translate(-50%, -50%) rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate(-50%, -50%) rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Rain icon */
|
/* Rain icon */
|
||||||
@ -309,7 +374,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.cloud::before {
|
.cloud::before {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -25px;
|
top: -25px;
|
||||||
left: 15px;
|
left: 15px;
|
||||||
@ -320,7 +385,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.cloud::after {
|
.cloud::after {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -15px;
|
top: -15px;
|
||||||
right: 15px;
|
right: 15px;
|
||||||
@ -348,13 +413,28 @@
|
|||||||
animation: rainDropFall 0.8s infinite ease-in;
|
animation: rainDropFall 0.8s infinite ease-in;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rain-drop-icon:nth-child(1) { left: 10px; animation-delay: 0s; }
|
.rain-drop-icon:nth-child(1) {
|
||||||
.rain-drop-icon:nth-child(2) { left: 25px; animation-delay: 0.2s; }
|
left: 10px;
|
||||||
.rain-drop-icon:nth-child(3) { left: 40px; animation-delay: 0.4s; }
|
animation-delay: 0s;
|
||||||
|
}
|
||||||
|
.rain-drop-icon:nth-child(2) {
|
||||||
|
left: 25px;
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
.rain-drop-icon:nth-child(3) {
|
||||||
|
left: 40px;
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes rainDropFall {
|
@keyframes rainDropFall {
|
||||||
0% { transform: translateY(0); opacity: 1; }
|
0% {
|
||||||
100% { transform: translateY(30px); opacity: 0; }
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(30px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Snow icon */
|
/* Snow icon */
|
||||||
@ -387,7 +467,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.snow-cloud::before {
|
.snow-cloud::before {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -22px;
|
top: -22px;
|
||||||
left: 12px;
|
left: 12px;
|
||||||
@ -398,7 +478,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.snow-cloud::after {
|
.snow-cloud::after {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -12px;
|
top: -12px;
|
||||||
right: 12px;
|
right: 12px;
|
||||||
@ -425,19 +505,33 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.snow-flake-icon::before {
|
.snow-flake-icon::before {
|
||||||
content: '❄';
|
content: "❄";
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: rgba(255, 255, 255, 0.9);
|
color: rgba(255, 255, 255, 0.9);
|
||||||
text-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
|
text-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.snow-flake-icon:nth-child(1) { left: 15px; animation-delay: 0s; }
|
.snow-flake-icon:nth-child(1) {
|
||||||
.snow-flake-icon:nth-child(2) { left: 35px; animation-delay: 0.6s; }
|
left: 15px;
|
||||||
.snow-flake-icon:nth-child(3) { left: 55px; animation-delay: 1.2s; }
|
animation-delay: 0s;
|
||||||
|
}
|
||||||
|
.snow-flake-icon:nth-child(2) {
|
||||||
|
left: 35px;
|
||||||
|
animation-delay: 0.6s;
|
||||||
|
}
|
||||||
|
.snow-flake-icon:nth-child(3) {
|
||||||
|
left: 55px;
|
||||||
|
animation-delay: 1.2s;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes snowFlakeFall {
|
@keyframes snowFlakeFall {
|
||||||
0%, 100% { transform: translateY(0) rotate(0deg); }
|
0%,
|
||||||
50% { transform: translateY(20px) rotate(180deg); }
|
100% {
|
||||||
|
transform: translateY(0) rotate(0deg);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(20px) rotate(180deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Weather info */
|
/* Weather info */
|
||||||
@ -447,7 +541,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.temperature {
|
.temperature {
|
||||||
font-family: 'Syncopate', sans-serif;
|
font-family: "Syncopate", sans-serif;
|
||||||
font-size: 72px;
|
font-size: 72px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
@ -526,7 +620,7 @@
|
|||||||
padding: 16px 32px;
|
padding: 16px 32px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
font-family: 'Space Grotesk', sans-serif;
|
font-family: "Space Grotesk", sans-serif;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@ -546,13 +640,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.weather-btn::before {
|
.weather-btn::before {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: -100%;
|
left: -100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent);
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
transparent,
|
||||||
|
rgba(255, 255, 255, 0.1),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
transition: left 0.5s ease;
|
transition: left 0.5s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -584,7 +683,7 @@
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 25px;
|
bottom: 25px;
|
||||||
right: 25px;
|
right: 25px;
|
||||||
font-family: 'Space Grotesk', sans-serif;
|
font-family: "Space Grotesk", sans-serif;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: rgba(255, 255, 255, 0.4);
|
color: rgba(255, 255, 255, 0.4);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@ -630,8 +729,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Background particles -->
|
<!-- Background particles -->
|
||||||
<div class="bg-particles" id="bgParticles"></div>
|
<div class="bg-particles" id="bgParticles"></div>
|
||||||
|
|
||||||
@ -646,7 +745,9 @@
|
|||||||
<div class="weather-card" id="weatherCard">
|
<div class="weather-card" id="weatherCard">
|
||||||
<div class="location">
|
<div class="location">
|
||||||
<svg class="location-icon" viewBox="0 0 24 24">
|
<svg class="location-icon" viewBox="0 0 24 24">
|
||||||
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"/>
|
<path
|
||||||
|
d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="location-text">San Francisco, CA</span>
|
<span class="location-text">San Francisco, CA</span>
|
||||||
</div>
|
</div>
|
||||||
@ -656,8 +757,14 @@
|
|||||||
<div class="icon-sun active" id="iconSun">
|
<div class="icon-sun active" id="iconSun">
|
||||||
<div class="sun-core"></div>
|
<div class="sun-core"></div>
|
||||||
<div class="sun-rays-icon">
|
<div class="sun-rays-icon">
|
||||||
<div class="sun-ray" style="transform: translate(-50%, -50%) rotate(45deg);"></div>
|
<div
|
||||||
<div class="sun-ray" style="transform: translate(-50%, -50%) rotate(135deg);"></div>
|
class="sun-ray"
|
||||||
|
style="transform: translate(-50%, -50%) rotate(45deg)"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="sun-ray"
|
||||||
|
style="transform: translate(-50%, -50%) rotate(135deg)"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -704,62 +811,74 @@
|
|||||||
|
|
||||||
<!-- Control Buttons -->
|
<!-- Control Buttons -->
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<button class="weather-btn active" id="btnSunny" onclick="setWeather('sunny')">Sunny</button>
|
<button
|
||||||
<button class="weather-btn" id="btnRainy" onclick="setWeather('rainy')">Rainy</button>
|
class="weather-btn active"
|
||||||
<button class="weather-btn" id="btnSnowy" onclick="setWeather('snowy')">Snowy</button>
|
id="btnSunny"
|
||||||
|
onclick="setWeather('sunny')"
|
||||||
|
>
|
||||||
|
Sunny
|
||||||
|
</button>
|
||||||
|
<button class="weather-btn" id="btnRainy" onclick="setWeather('rainy')">
|
||||||
|
Rainy
|
||||||
|
</button>
|
||||||
|
<button class="weather-btn" id="btnSnowy" onclick="setWeather('snowy')">
|
||||||
|
Snowy
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Deerflow Signature -->
|
<!-- Deerflow Signature -->
|
||||||
<a href="https://deerflow.tech" target="_blank" class="deerflow-signature">✦ Deerflow</a>
|
<a href="https://deerflow.tech" target="_blank" class="deerflow-signature"
|
||||||
|
>✦ Deerflow</a
|
||||||
|
>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Weather data
|
// Weather data
|
||||||
const weatherData = {
|
const weatherData = {
|
||||||
sunny: {
|
sunny: {
|
||||||
temp: '72°',
|
temp: "72°",
|
||||||
condition: 'Sunny',
|
condition: "Sunny",
|
||||||
humidity: '45%',
|
humidity: "45%",
|
||||||
wind: '8 mph',
|
wind: "8 mph",
|
||||||
uvIndex: 'High',
|
uvIndex: "High",
|
||||||
bgClass: 'weather-sunny'
|
bgClass: "weather-sunny",
|
||||||
},
|
},
|
||||||
rainy: {
|
rainy: {
|
||||||
temp: '58°',
|
temp: "58°",
|
||||||
condition: 'Rainy',
|
condition: "Rainy",
|
||||||
humidity: '82%',
|
humidity: "82%",
|
||||||
wind: '15 mph',
|
wind: "15 mph",
|
||||||
uvIndex: 'Low',
|
uvIndex: "Low",
|
||||||
bgClass: 'weather-rainy'
|
bgClass: "weather-rainy",
|
||||||
},
|
},
|
||||||
snowy: {
|
snowy: {
|
||||||
temp: '28°',
|
temp: "28°",
|
||||||
condition: 'Snowy',
|
condition: "Snowy",
|
||||||
humidity: '68%',
|
humidity: "68%",
|
||||||
wind: '12 mph',
|
wind: "12 mph",
|
||||||
uvIndex: 'Low',
|
uvIndex: "Low",
|
||||||
bgClass: 'weather-snowy'
|
bgClass: "weather-snowy",
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// DOM elements
|
// DOM elements
|
||||||
const weatherCard = document.getElementById('weatherCard');
|
const weatherCard = document.getElementById("weatherCard");
|
||||||
const weatherEffects = document.getElementById('weatherEffects');
|
const weatherEffects = document.getElementById("weatherEffects");
|
||||||
const temperature = document.getElementById('temperature');
|
const temperature = document.getElementById("temperature");
|
||||||
const condition = document.getElementById('condition');
|
const condition = document.getElementById("condition");
|
||||||
const humidity = document.getElementById('humidity');
|
const humidity = document.getElementById("humidity");
|
||||||
const wind = document.getElementById('wind');
|
const wind = document.getElementById("wind");
|
||||||
const uvIndex = document.getElementById('uvIndex');
|
const uvIndex = document.getElementById("uvIndex");
|
||||||
|
|
||||||
// Icon elements
|
// Icon elements
|
||||||
const iconSun = document.getElementById('iconSun');
|
const iconSun = document.getElementById("iconSun");
|
||||||
const iconRain = document.getElementById('iconRain');
|
const iconRain = document.getElementById("iconRain");
|
||||||
const iconSnow = document.getElementById('iconSnow');
|
const iconSnow = document.getElementById("iconSnow");
|
||||||
|
|
||||||
// Button elements
|
// Button elements
|
||||||
const btnSunny = document.getElementById('btnSunny');
|
const btnSunny = document.getElementById("btnSunny");
|
||||||
const btnRainy = document.getElementById('btnRainy');
|
const btnRainy = document.getElementById("btnRainy");
|
||||||
const btnSnowy = document.getElementById('btnSnowy');
|
const btnSnowy = document.getElementById("btnSnowy");
|
||||||
|
|
||||||
// 3D Tilt effect
|
// 3D Tilt effect
|
||||||
let bounds;
|
let bounds;
|
||||||
@ -773,7 +892,7 @@
|
|||||||
const topY = mouseY - bounds.y;
|
const topY = mouseY - bounds.y;
|
||||||
const center = {
|
const center = {
|
||||||
x: leftX - bounds.width / 2,
|
x: leftX - bounds.width / 2,
|
||||||
y: topY - bounds.height / 2
|
y: topY - bounds.height / 2,
|
||||||
};
|
};
|
||||||
const distance = Math.sqrt(center.x ** 2 + center.y ** 2);
|
const distance = Math.sqrt(center.x ** 2 + center.y ** 2);
|
||||||
|
|
||||||
@ -795,7 +914,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resetTilt() {
|
function resetTilt() {
|
||||||
weatherCard.style.transform = 'perspective(1200px) rotateX(0) rotateY(0) scale3d(1, 1, 1)';
|
weatherCard.style.transform =
|
||||||
|
"perspective(1200px) rotateX(0) rotateY(0) scale3d(1, 1, 1)";
|
||||||
weatherCard.style.boxShadow = `
|
weatherCard.style.boxShadow = `
|
||||||
0 25px 50px -12px var(--glass-shadow),
|
0 25px 50px -12px var(--glass-shadow),
|
||||||
inset 0 1px 1px var(--glass-highlight),
|
inset 0 1px 1px var(--glass-highlight),
|
||||||
@ -804,17 +924,17 @@
|
|||||||
bounds = null;
|
bounds = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
weatherCard.addEventListener('mousemove', rotateToMouse);
|
weatherCard.addEventListener("mousemove", rotateToMouse);
|
||||||
weatherCard.addEventListener('mouseleave', resetTilt);
|
weatherCard.addEventListener("mouseleave", resetTilt);
|
||||||
window.addEventListener('resize', () => bounds = null);
|
window.addEventListener("resize", () => (bounds = null));
|
||||||
|
|
||||||
// Set weather function
|
// Set weather function
|
||||||
function setWeather(type) {
|
function setWeather(type) {
|
||||||
const data = weatherData[type];
|
const data = weatherData[type];
|
||||||
|
|
||||||
// Update text with animation
|
// Update text with animation
|
||||||
temperature.style.opacity = '0';
|
temperature.style.opacity = "0";
|
||||||
condition.style.opacity = '0';
|
condition.style.opacity = "0";
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
temperature.textContent = data.temp;
|
temperature.textContent = data.temp;
|
||||||
@ -823,33 +943,33 @@
|
|||||||
wind.textContent = data.wind;
|
wind.textContent = data.wind;
|
||||||
uvIndex.textContent = data.uvIndex;
|
uvIndex.textContent = data.uvIndex;
|
||||||
|
|
||||||
temperature.style.opacity = '1';
|
temperature.style.opacity = "1";
|
||||||
condition.style.opacity = '1';
|
condition.style.opacity = "1";
|
||||||
}, 250);
|
}, 250);
|
||||||
|
|
||||||
// Update icons
|
// Update icons
|
||||||
iconSun.classList.remove('active');
|
iconSun.classList.remove("active");
|
||||||
iconRain.classList.remove('active');
|
iconRain.classList.remove("active");
|
||||||
iconSnow.classList.remove('active');
|
iconSnow.classList.remove("active");
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (type === 'sunny') iconSun.classList.add('active');
|
if (type === "sunny") iconSun.classList.add("active");
|
||||||
if (type === 'rainy') iconRain.classList.add('active');
|
if (type === "rainy") iconRain.classList.add("active");
|
||||||
if (type === 'snowy') iconSnow.classList.add('active');
|
if (type === "snowy") iconSnow.classList.add("active");
|
||||||
}, 200);
|
}, 200);
|
||||||
|
|
||||||
// Update background weather effects
|
// Update background weather effects
|
||||||
document.body.className = '';
|
document.body.className = "";
|
||||||
document.body.classList.add(data.bgClass);
|
document.body.classList.add(data.bgClass);
|
||||||
|
|
||||||
// Update buttons
|
// Update buttons
|
||||||
btnSunny.classList.remove('active');
|
btnSunny.classList.remove("active");
|
||||||
btnRainy.classList.remove('active');
|
btnRainy.classList.remove("active");
|
||||||
btnSnowy.classList.remove('active');
|
btnSnowy.classList.remove("active");
|
||||||
|
|
||||||
if (type === 'sunny') btnSunny.classList.add('active');
|
if (type === "sunny") btnSunny.classList.add("active");
|
||||||
if (type === 'rainy') btnRainy.classList.add('active');
|
if (type === "rainy") btnRainy.classList.add("active");
|
||||||
if (type === 'snowy') btnSnowy.classList.add('active');
|
if (type === "snowy") btnSnowy.classList.add("active");
|
||||||
|
|
||||||
// Update weather effects
|
// Update weather effects
|
||||||
updateWeatherEffects(type);
|
updateWeatherEffects(type);
|
||||||
@ -858,26 +978,28 @@
|
|||||||
// Create weather effects
|
// Create weather effects
|
||||||
function updateWeatherEffects(type) {
|
function updateWeatherEffects(type) {
|
||||||
// Clear existing effects
|
// Clear existing effects
|
||||||
const existingEffects = weatherEffects.querySelectorAll('.rain-drop, .snow-flake');
|
const existingEffects = weatherEffects.querySelectorAll(
|
||||||
existingEffects.forEach(el => el.remove());
|
".rain-drop, .snow-flake",
|
||||||
|
);
|
||||||
|
existingEffects.forEach((el) => el.remove());
|
||||||
|
|
||||||
if (type === 'rainy') {
|
if (type === "rainy") {
|
||||||
for (let i = 0; i < 50; i++) {
|
for (let i = 0; i < 50; i++) {
|
||||||
const drop = document.createElement('div');
|
const drop = document.createElement("div");
|
||||||
drop.className = 'rain-drop';
|
drop.className = "rain-drop";
|
||||||
drop.style.left = Math.random() * 100 + '%';
|
drop.style.left = Math.random() * 100 + "%";
|
||||||
drop.style.animationDelay = Math.random() * 2 + 's';
|
drop.style.animationDelay = Math.random() * 2 + "s";
|
||||||
drop.style.animationDuration = (0.5 + Math.random() * 0.5) + 's';
|
drop.style.animationDuration = 0.5 + Math.random() * 0.5 + "s";
|
||||||
weatherEffects.appendChild(drop);
|
weatherEffects.appendChild(drop);
|
||||||
}
|
}
|
||||||
} else if (type === 'snowy') {
|
} else if (type === "snowy") {
|
||||||
for (let i = 0; i < 40; i++) {
|
for (let i = 0; i < 40; i++) {
|
||||||
const flake = document.createElement('div');
|
const flake = document.createElement("div");
|
||||||
flake.className = 'snow-flake';
|
flake.className = "snow-flake";
|
||||||
flake.style.left = Math.random() * 100 + '%';
|
flake.style.left = Math.random() * 100 + "%";
|
||||||
flake.style.animationDelay = Math.random() * 3 + 's';
|
flake.style.animationDelay = Math.random() * 3 + "s";
|
||||||
flake.style.animationDuration = (2 + Math.random() * 2) + 's';
|
flake.style.animationDuration = 2 + Math.random() * 2 + "s";
|
||||||
flake.style.width = (4 + Math.random() * 6) + 'px';
|
flake.style.width = 4 + Math.random() * 6 + "px";
|
||||||
flake.style.height = flake.style.width;
|
flake.style.height = flake.style.width;
|
||||||
weatherEffects.appendChild(flake);
|
weatherEffects.appendChild(flake);
|
||||||
}
|
}
|
||||||
@ -886,20 +1008,20 @@
|
|||||||
|
|
||||||
// Create background particles
|
// Create background particles
|
||||||
function createParticles() {
|
function createParticles() {
|
||||||
const container = document.getElementById('bgParticles');
|
const container = document.getElementById("bgParticles");
|
||||||
for (let i = 0; i < 30; i++) {
|
for (let i = 0; i < 30; i++) {
|
||||||
const particle = document.createElement('div');
|
const particle = document.createElement("div");
|
||||||
particle.className = 'particle';
|
particle.className = "particle";
|
||||||
particle.style.left = Math.random() * 100 + '%';
|
particle.style.left = Math.random() * 100 + "%";
|
||||||
particle.style.animationDelay = Math.random() * 15 + 's';
|
particle.style.animationDelay = Math.random() * 15 + "s";
|
||||||
particle.style.animationDuration = (10 + Math.random() * 10) + 's';
|
particle.style.animationDuration = 10 + Math.random() * 10 + "s";
|
||||||
container.appendChild(particle);
|
container.appendChild(particle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize
|
// Initialize
|
||||||
createParticles();
|
createParticles();
|
||||||
document.body.classList.add('weather-sunny');
|
document.body.classList.add("weather-sunny");
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -1,12 +1,15 @@
|
|||||||
<!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>DeerFlow Research Report 2026</title>
|
<title>DeerFlow Research Report 2026</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=Playfair+Display:ital,wght@0,400;0,600;0,700;1,400&family=JetBrains+Mono:wght@300;400;500&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;0,700;1,400&family=JetBrains+Mono:wght@300;400;500&family=Inter:wght@300;400;500;600&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--bg-primary: #0a0a0f;
|
--bg-primary: #0a0a0f;
|
||||||
@ -20,7 +23,11 @@
|
|||||||
--text-muted: #606070;
|
--text-muted: #606070;
|
||||||
--border-subtle: rgba(255, 255, 255, 0.08);
|
--border-subtle: rgba(255, 255, 255, 0.08);
|
||||||
--gradient-1: linear-gradient(135deg, #00d4aa 0%, #00a8e8 100%);
|
--gradient-1: linear-gradient(135deg, #00d4aa 0%, #00a8e8 100%);
|
||||||
--gradient-2: linear-gradient(180deg, transparent 0%, rgba(0, 212, 170, 0.03) 100%);
|
--gradient-2: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
transparent 0%,
|
||||||
|
rgba(0, 212, 170, 0.03) 100%
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
@ -34,7 +41,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: "Inter", sans-serif;
|
||||||
background: var(--bg-primary);
|
background: var(--bg-primary);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
@ -61,20 +68,43 @@
|
|||||||
width: 600px;
|
width: 600px;
|
||||||
height: 600px;
|
height: 600px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: radial-gradient(circle, var(--accent-glow) 0%, transparent 70%);
|
background: radial-gradient(
|
||||||
|
circle,
|
||||||
|
var(--accent-glow) 0%,
|
||||||
|
transparent 70%
|
||||||
|
);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
animation: float 20s ease-in-out infinite;
|
animation: float 20s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg-glow:nth-child(1) { top: -200px; left: -200px; animation-delay: 0s; }
|
.bg-glow:nth-child(1) {
|
||||||
.bg-glow:nth-child(2) { top: 50%; right: -300px; animation-delay: -7s; }
|
top: -200px;
|
||||||
.bg-glow:nth-child(3) { bottom: -200px; left: 30%; animation-delay: -14s; }
|
left: -200px;
|
||||||
|
animation-delay: 0s;
|
||||||
|
}
|
||||||
|
.bg-glow:nth-child(2) {
|
||||||
|
top: 50%;
|
||||||
|
right: -300px;
|
||||||
|
animation-delay: -7s;
|
||||||
|
}
|
||||||
|
.bg-glow:nth-child(3) {
|
||||||
|
bottom: -200px;
|
||||||
|
left: 30%;
|
||||||
|
animation-delay: -14s;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes float {
|
@keyframes float {
|
||||||
0%, 100% { transform: translate(0, 0) scale(1); }
|
0%,
|
||||||
33% { transform: translate(30px, -30px) scale(1.1); }
|
100% {
|
||||||
66% { transform: translate(-20px, 20px) scale(0.95); }
|
transform: translate(0, 0) scale(1);
|
||||||
|
}
|
||||||
|
33% {
|
||||||
|
transform: translate(30px, -30px) scale(1.1);
|
||||||
|
}
|
||||||
|
66% {
|
||||||
|
transform: translate(-20px, 20px) scale(0.95);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Navigation */
|
/* Navigation */
|
||||||
@ -88,7 +118,11 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
background: linear-gradient(180deg, var(--bg-primary) 0%, transparent 100%);
|
background: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
var(--bg-primary) 0%,
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,7 +130,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
font-family: 'Playfair Display', serif;
|
font-family: "Playfair Display", serif;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
@ -134,7 +168,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.nav-links a::after {
|
.nav-links a::after {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: -4px;
|
bottom: -4px;
|
||||||
left: 0;
|
left: 0;
|
||||||
@ -172,7 +206,7 @@
|
|||||||
background: var(--bg-tertiary);
|
background: var(--bg-tertiary);
|
||||||
border: 1px solid var(--border-subtle);
|
border: 1px solid var(--border-subtle);
|
||||||
border-radius: 100px;
|
border-radius: 100px;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: "JetBrains Mono", monospace;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: var(--accent-primary);
|
color: var(--accent-primary);
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
@ -180,7 +214,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.hero-badge::before {
|
.hero-badge::before {
|
||||||
content: '';
|
content: "";
|
||||||
width: 6px;
|
width: 6px;
|
||||||
height: 6px;
|
height: 6px;
|
||||||
background: var(--accent-primary);
|
background: var(--accent-primary);
|
||||||
@ -189,12 +223,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0%, 100% { opacity: 1; }
|
0%,
|
||||||
50% { opacity: 0.5; }
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero h1 {
|
.hero h1 {
|
||||||
font-family: 'Playfair Display', serif;
|
font-family: "Playfair Display", serif;
|
||||||
font-size: clamp(3rem, 8vw, 6rem);
|
font-size: clamp(3rem, 8vw, 6rem);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
@ -233,7 +272,9 @@
|
|||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
border: 1px solid var(--border-subtle);
|
border: 1px solid var(--border-subtle);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
transition: transform 0.3s ease, border-color 0.3s ease;
|
transition:
|
||||||
|
transform 0.3s ease,
|
||||||
|
border-color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-item:hover {
|
.stat-item:hover {
|
||||||
@ -242,7 +283,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.stat-value {
|
.stat-value {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: "JetBrains Mono", monospace;
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--accent-primary);
|
color: var(--accent-primary);
|
||||||
@ -268,7 +309,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.section-tag {
|
.section-tag {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: "JetBrains Mono", monospace;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: var(--accent-secondary);
|
color: var(--accent-secondary);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@ -277,7 +318,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
font-family: 'Playfair Display', serif;
|
font-family: "Playfair Display", serif;
|
||||||
font-size: clamp(2rem, 5vw, 3.5rem);
|
font-size: clamp(2rem, 5vw, 3.5rem);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
@ -300,7 +341,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.summary-card::before {
|
.summary-card::before {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
@ -327,13 +368,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.timeline::before {
|
.timeline::before {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
width: 2px;
|
width: 2px;
|
||||||
background: linear-gradient(180deg, var(--accent-primary) 0%, var(--accent-secondary) 100%);
|
background: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
var(--accent-primary) 0%,
|
||||||
|
var(--accent-secondary) 100%
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-item {
|
.timeline-item {
|
||||||
@ -358,7 +403,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.timeline-phase {
|
.timeline-phase {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: "JetBrains Mono", monospace;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: var(--accent-primary);
|
color: var(--accent-primary);
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
@ -371,7 +416,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.timeline-title {
|
.timeline-title {
|
||||||
font-family: 'Playfair Display', serif;
|
font-family: "Playfair Display", serif;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
@ -427,7 +472,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.arch-node-title {
|
.arch-node-title {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: "JetBrains Mono", monospace;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
@ -441,12 +486,16 @@
|
|||||||
.arch-connector {
|
.arch-connector {
|
||||||
width: 2px;
|
width: 2px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
background: linear-gradient(180deg, var(--accent-primary), var(--accent-secondary));
|
background: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
var(--accent-primary),
|
||||||
|
var(--accent-secondary)
|
||||||
|
);
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arch-connector::after {
|
.arch-connector::after {
|
||||||
content: '▼';
|
content: "▼";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: -5px;
|
bottom: -5px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@ -462,14 +511,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.arch-branch::before {
|
.arch-branch::before {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -15px;
|
top: -15px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
width: 60%;
|
width: 60%;
|
||||||
height: 2px;
|
height: 2px;
|
||||||
background: linear-gradient(90deg, transparent, var(--accent-primary), transparent);
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
transparent,
|
||||||
|
var(--accent-primary),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Features Grid */
|
/* Features Grid */
|
||||||
@ -505,7 +559,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.feature-title {
|
.feature-title {
|
||||||
font-family: 'Playfair Display', serif;
|
font-family: "Playfair Display", serif;
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.75rem;
|
||||||
}
|
}
|
||||||
@ -532,7 +586,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.comparison-table th {
|
.comparison-table th {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: "JetBrains Mono", monospace;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
@ -594,7 +648,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.swot-title {
|
.swot-title {
|
||||||
font-family: 'Playfair Display', serif;
|
font-family: "Playfair Display", serif;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -620,7 +674,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.swot-list li::before {
|
.swot-list li::before {
|
||||||
content: '→';
|
content: "→";
|
||||||
color: var(--accent-primary);
|
color: var(--accent-primary);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
@ -647,7 +701,7 @@
|
|||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
border: 1px solid var(--border-subtle);
|
border: 1px solid var(--border-subtle);
|
||||||
border-radius: 100px;
|
border-radius: 100px;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: "JetBrains Mono", monospace;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@ -666,7 +720,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.deerflow-badge::before {
|
.deerflow-badge::before {
|
||||||
content: '✦';
|
content: "✦";
|
||||||
color: var(--accent-primary);
|
color: var(--accent-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -718,7 +772,9 @@
|
|||||||
.fade-in {
|
.fade-in {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(30px);
|
transform: translateY(30px);
|
||||||
transition: opacity 0.8s ease, transform 0.8s ease;
|
transition:
|
||||||
|
opacity 0.8s ease,
|
||||||
|
transform 0.8s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-in.visible {
|
.fade-in.visible {
|
||||||
@ -728,7 +784,7 @@
|
|||||||
|
|
||||||
/* Code Block Style */
|
/* Code Block Style */
|
||||||
code {
|
code {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: "JetBrains Mono", monospace;
|
||||||
background: var(--bg-tertiary);
|
background: var(--bg-tertiary);
|
||||||
padding: 0.2rem 0.5rem;
|
padding: 0.2rem 0.5rem;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@ -736,8 +792,8 @@
|
|||||||
color: var(--accent-primary);
|
color: var(--accent-primary);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="bg-grid"></div>
|
<div class="bg-grid"></div>
|
||||||
<div class="bg-glow"></div>
|
<div class="bg-glow"></div>
|
||||||
<div class="bg-glow"></div>
|
<div class="bg-glow"></div>
|
||||||
@ -760,8 +816,12 @@
|
|||||||
<main>
|
<main>
|
||||||
<section class="hero">
|
<section class="hero">
|
||||||
<div class="hero-badge">Research Report 2026</div>
|
<div class="hero-badge">Research Report 2026</div>
|
||||||
<h1>DeerFlow:<br><em>Multi-Agent Deep Research</em></h1>
|
<h1>DeerFlow:<br /><em>Multi-Agent Deep Research</em></h1>
|
||||||
<p class="hero-subtitle">A comprehensive analysis of ByteDance's open-source framework that combines language models with specialized tools for automated research workflows.</p>
|
<p class="hero-subtitle">
|
||||||
|
A comprehensive analysis of ByteDance's open-source framework that
|
||||||
|
combines language models with specialized tools for automated research
|
||||||
|
workflows.
|
||||||
|
</p>
|
||||||
<div class="hero-stats">
|
<div class="hero-stats">
|
||||||
<div class="stat-item">
|
<div class="stat-item">
|
||||||
<div class="stat-value" data-count="19531">0</div>
|
<div class="stat-value" data-count="19531">0</div>
|
||||||
@ -786,13 +846,26 @@
|
|||||||
<div class="section-header fade-in">
|
<div class="section-header fade-in">
|
||||||
<div class="section-tag">01 / Overview</div>
|
<div class="section-tag">01 / Overview</div>
|
||||||
<h2 class="section-title">Executive Summary</h2>
|
<h2 class="section-title">Executive Summary</h2>
|
||||||
<p class="section-desc">The framework that redefines automated research through intelligent multi-agent orchestration.</p>
|
<p class="section-desc">
|
||||||
|
The framework that redefines automated research through intelligent
|
||||||
|
multi-agent orchestration.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-card fade-in">
|
<div class="summary-card fade-in">
|
||||||
<p class="summary-text">
|
<p class="summary-text">
|
||||||
<strong>DeerFlow</strong> (Deep Exploration and Efficient Research Flow) is an open-source multi-agent research automation framework developed by ByteDance and released under the MIT license in May 2025. The framework implements a <strong>graph-based orchestration</strong> of specialized agents that automate research pipelines end-to-end, combining language models with tools like web search engines, crawlers, and Python execution.
|
<strong>DeerFlow</strong> (Deep Exploration and Efficient Research
|
||||||
<br><br>
|
Flow) is an open-source multi-agent research automation framework
|
||||||
With <strong>19,531 stars</strong> and <strong>2,452 forks</strong> on GitHub, DeerFlow has established itself as a significant player in the deep research automation space, offering both console and web UI options with support for local LLM deployment and extensive tool integrations.
|
developed by ByteDance and released under the MIT license in May
|
||||||
|
2025. The framework implements a
|
||||||
|
<strong>graph-based orchestration</strong> of specialized agents
|
||||||
|
that automate research pipelines end-to-end, combining language
|
||||||
|
models with tools like web search engines, crawlers, and Python
|
||||||
|
execution. <br /><br />
|
||||||
|
With <strong>19,531 stars</strong> and
|
||||||
|
<strong>2,452 forks</strong> on GitHub, DeerFlow has established
|
||||||
|
itself as a significant player in the deep research automation
|
||||||
|
space, offering both console and web UI options with support for
|
||||||
|
local LLM deployment and extensive tool integrations.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -801,7 +874,9 @@
|
|||||||
<div class="section-header fade-in">
|
<div class="section-header fade-in">
|
||||||
<div class="section-tag">02 / History</div>
|
<div class="section-tag">02 / History</div>
|
||||||
<h2 class="section-title">Development Timeline</h2>
|
<h2 class="section-title">Development Timeline</h2>
|
||||||
<p class="section-desc">From initial release to the upcoming DeerFlow 2.0 transition.</p>
|
<p class="section-desc">
|
||||||
|
From initial release to the upcoming DeerFlow 2.0 transition.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="timeline fade-in">
|
<div class="timeline fade-in">
|
||||||
<div class="timeline-item">
|
<div class="timeline-item">
|
||||||
@ -809,21 +884,39 @@
|
|||||||
<div class="timeline-phase">Phase 01</div>
|
<div class="timeline-phase">Phase 01</div>
|
||||||
<div class="timeline-date">May — July 2025</div>
|
<div class="timeline-date">May — July 2025</div>
|
||||||
<h3 class="timeline-title">Project Inception</h3>
|
<h3 class="timeline-title">Project Inception</h3>
|
||||||
<p class="timeline-content">DeerFlow was created by ByteDance and open-sourced on May 7, 2025. The initial release established the core multi-agent architecture built on LangGraph and LangChain frameworks, featuring specialized agents: Coordinator, Planner, Researcher, Coder, and Reporter.</p>
|
<p class="timeline-content">
|
||||||
|
DeerFlow was created by ByteDance and open-sourced on May 7, 2025.
|
||||||
|
The initial release established the core multi-agent architecture
|
||||||
|
built on LangGraph and LangChain frameworks, featuring specialized
|
||||||
|
agents: Coordinator, Planner, Researcher, Coder, and Reporter.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="timeline-item">
|
<div class="timeline-item">
|
||||||
<div class="timeline-dot"></div>
|
<div class="timeline-dot"></div>
|
||||||
<div class="timeline-phase">Phase 02</div>
|
<div class="timeline-phase">Phase 02</div>
|
||||||
<div class="timeline-date">August — December 2025</div>
|
<div class="timeline-date">August — December 2025</div>
|
||||||
<h3 class="timeline-title">Feature Expansion</h3>
|
<h3 class="timeline-title">Feature Expansion</h3>
|
||||||
<p class="timeline-content">Major feature additions including MCP integration, text-to-speech capabilities, podcast generation, and support for multiple search engines (Tavily, InfoQuest, Brave Search, DuckDuckGo, Arxiv). The framework gained recognition for its human-in-the-loop collaboration features and was integrated into Volcengine's FaaS Application Center.</p>
|
<p class="timeline-content">
|
||||||
|
Major feature additions including MCP integration, text-to-speech
|
||||||
|
capabilities, podcast generation, and support for multiple search
|
||||||
|
engines (Tavily, InfoQuest, Brave Search, DuckDuckGo, Arxiv). The
|
||||||
|
framework gained recognition for its human-in-the-loop
|
||||||
|
collaboration features and was integrated into Volcengine's FaaS
|
||||||
|
Application Center.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="timeline-item">
|
<div class="timeline-item">
|
||||||
<div class="timeline-dot"></div>
|
<div class="timeline-dot"></div>
|
||||||
<div class="timeline-phase">Phase 03</div>
|
<div class="timeline-phase">Phase 03</div>
|
||||||
<div class="timeline-date">January 2026 — Present</div>
|
<div class="timeline-date">January 2026 — Present</div>
|
||||||
<h3 class="timeline-title">DeerFlow 2.0 Transition</h3>
|
<h3 class="timeline-title">DeerFlow 2.0 Transition</h3>
|
||||||
<p class="timeline-content">The project is transitioning to DeerFlow 2.0 with ongoing improvements to JSON repair handling, MCP tool integration, and fallback report generation. Now supports private knowledgebases including RAGFlow, Qdrant, Milvus, and VikingDB, along with comprehensive Docker deployment options.</p>
|
<p class="timeline-content">
|
||||||
|
The project is transitioning to DeerFlow 2.0 with ongoing
|
||||||
|
improvements to JSON repair handling, MCP tool integration, and
|
||||||
|
fallback report generation. Now supports private knowledgebases
|
||||||
|
including RAGFlow, Qdrant, Milvus, and VikingDB, along with
|
||||||
|
comprehensive Docker deployment options.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -832,7 +925,10 @@
|
|||||||
<div class="section-header fade-in">
|
<div class="section-header fade-in">
|
||||||
<div class="section-tag">03 / System Design</div>
|
<div class="section-tag">03 / System Design</div>
|
||||||
<h2 class="section-title">Multi-Agent Architecture</h2>
|
<h2 class="section-title">Multi-Agent Architecture</h2>
|
||||||
<p class="section-desc">A modular system built on LangGraph enabling flexible state-based workflows.</p>
|
<p class="section-desc">
|
||||||
|
A modular system built on LangGraph enabling flexible state-based
|
||||||
|
workflows.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="architecture-container fade-in">
|
<div class="architecture-container fade-in">
|
||||||
<div class="arch-flow">
|
<div class="arch-flow">
|
||||||
@ -869,38 +965,58 @@
|
|||||||
<div class="section-header fade-in">
|
<div class="section-header fade-in">
|
||||||
<div class="section-tag">04 / Capabilities</div>
|
<div class="section-tag">04 / Capabilities</div>
|
||||||
<h2 class="section-title">Key Features</h2>
|
<h2 class="section-title">Key Features</h2>
|
||||||
<p class="section-desc">Comprehensive tooling for end-to-end research automation.</p>
|
<p class="section-desc">
|
||||||
|
Comprehensive tooling for end-to-end research automation.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="features-grid">
|
<div class="features-grid">
|
||||||
<div class="feature-card fade-in">
|
<div class="feature-card fade-in">
|
||||||
<div class="feature-icon">🔍</div>
|
<div class="feature-icon">🔍</div>
|
||||||
<h3 class="feature-title">Multi-Engine Search</h3>
|
<h3 class="feature-title">Multi-Engine Search</h3>
|
||||||
<p class="feature-desc">Supports Tavily, InfoQuest (BytePlus), Brave Search, DuckDuckGo, and Arxiv for scientific papers with configurable parameters.</p>
|
<p class="feature-desc">
|
||||||
|
Supports Tavily, InfoQuest (BytePlus), Brave Search, DuckDuckGo,
|
||||||
|
and Arxiv for scientific papers with configurable parameters.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="feature-card fade-in">
|
<div class="feature-card fade-in">
|
||||||
<div class="feature-icon">🔗</div>
|
<div class="feature-icon">🔗</div>
|
||||||
<h3 class="feature-title">MCP Integration</h3>
|
<h3 class="feature-title">MCP Integration</h3>
|
||||||
<p class="feature-desc">Seamless integration with Model Context Protocol services for private domain access, knowledge graphs, and web browsing.</p>
|
<p class="feature-desc">
|
||||||
|
Seamless integration with Model Context Protocol services for
|
||||||
|
private domain access, knowledge graphs, and web browsing.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="feature-card fade-in">
|
<div class="feature-card fade-in">
|
||||||
<div class="feature-icon">📚</div>
|
<div class="feature-icon">📚</div>
|
||||||
<h3 class="feature-title">Private Knowledgebase</h3>
|
<h3 class="feature-title">Private Knowledgebase</h3>
|
||||||
<p class="feature-desc">Integrates with RAGFlow, Qdrant, Milvus, VikingDB, MOI, and Dify for research on users' private documents.</p>
|
<p class="feature-desc">
|
||||||
|
Integrates with RAGFlow, Qdrant, Milvus, VikingDB, MOI, and Dify
|
||||||
|
for research on users' private documents.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="feature-card fade-in">
|
<div class="feature-card fade-in">
|
||||||
<div class="feature-icon">🤝</div>
|
<div class="feature-icon">🤝</div>
|
||||||
<h3 class="feature-title">Human-in-the-Loop</h3>
|
<h3 class="feature-title">Human-in-the-Loop</h3>
|
||||||
<p class="feature-desc">Intelligent clarification mechanisms, plan review and editing, and auto-acceptance options for streamlined workflows.</p>
|
<p class="feature-desc">
|
||||||
|
Intelligent clarification mechanisms, plan review and editing, and
|
||||||
|
auto-acceptance options for streamlined workflows.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="feature-card fade-in">
|
<div class="feature-card fade-in">
|
||||||
<div class="feature-icon">🎙️</div>
|
<div class="feature-icon">🎙️</div>
|
||||||
<h3 class="feature-title">Content Creation</h3>
|
<h3 class="feature-title">Content Creation</h3>
|
||||||
<p class="feature-desc">Podcast generation with TTS synthesis, PowerPoint creation, and Notion-style block editing for report refinement.</p>
|
<p class="feature-desc">
|
||||||
|
Podcast generation with TTS synthesis, PowerPoint creation, and
|
||||||
|
Notion-style block editing for report refinement.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="feature-card fade-in">
|
<div class="feature-card fade-in">
|
||||||
<div class="feature-icon">🐳</div>
|
<div class="feature-icon">🐳</div>
|
||||||
<h3 class="feature-title">Production Ready</h3>
|
<h3 class="feature-title">Production Ready</h3>
|
||||||
<p class="feature-desc">Docker and Docker Compose support, cloud deployment via Volcengine, and comprehensive API documentation.</p>
|
<p class="feature-desc">
|
||||||
|
Docker and Docker Compose support, cloud deployment via
|
||||||
|
Volcengine, and comprehensive API documentation.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -909,9 +1025,11 @@
|
|||||||
<div class="section-header fade-in">
|
<div class="section-header fade-in">
|
||||||
<div class="section-tag">05 / Analysis</div>
|
<div class="section-tag">05 / Analysis</div>
|
||||||
<h2 class="section-title">Competitive Comparison</h2>
|
<h2 class="section-title">Competitive Comparison</h2>
|
||||||
<p class="section-desc">How DeerFlow compares to other deep research solutions.</p>
|
<p class="section-desc">
|
||||||
|
How DeerFlow compares to other deep research solutions.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div style="overflow-x: auto;" class="fade-in">
|
<div style="overflow-x: auto" class="fade-in">
|
||||||
<table class="comparison-table">
|
<table class="comparison-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@ -979,16 +1097,25 @@
|
|||||||
<div class="section-header fade-in">
|
<div class="section-header fade-in">
|
||||||
<div class="section-tag">06 / Assessment</div>
|
<div class="section-tag">06 / Assessment</div>
|
||||||
<h2 class="section-title">Strengths & Considerations</h2>
|
<h2 class="section-title">Strengths & Considerations</h2>
|
||||||
<p class="section-desc">Balanced evaluation of the framework's capabilities.</p>
|
<p class="section-desc">
|
||||||
|
Balanced evaluation of the framework's capabilities.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="swot-grid fade-in">
|
<div class="swot-grid fade-in">
|
||||||
<div class="swot-card strengths">
|
<div class="swot-card strengths">
|
||||||
<h3 class="swot-title">💪 Strengths</h3>
|
<h3 class="swot-title">💪 Strengths</h3>
|
||||||
<ul class="swot-list">
|
<ul class="swot-list">
|
||||||
<li>Comprehensive multi-agent architecture with specialized roles</li>
|
<li>
|
||||||
<li>Extensive tool integration across search, crawling, and databases</li>
|
Comprehensive multi-agent architecture with specialized roles
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Extensive tool integration across search, crawling, and
|
||||||
|
databases
|
||||||
|
</li>
|
||||||
<li>Local LLM deployment support for privacy and cost control</li>
|
<li>Local LLM deployment support for privacy and cost control</li>
|
||||||
<li>Human collaboration features bridging automation and oversight</li>
|
<li>
|
||||||
|
Human collaboration features bridging automation and oversight
|
||||||
|
</li>
|
||||||
<li>Active community with 88+ contributors</li>
|
<li>Active community with 88+ contributors</li>
|
||||||
<li>Production-ready with Docker and cloud deployment options</li>
|
<li>Production-ready with Docker and cloud deployment options</li>
|
||||||
</ul>
|
</ul>
|
||||||
@ -996,10 +1123,20 @@
|
|||||||
<div class="swot-card weaknesses">
|
<div class="swot-card weaknesses">
|
||||||
<h3 class="swot-title">⚠️ Considerations</h3>
|
<h3 class="swot-title">⚠️ Considerations</h3>
|
||||||
<ul class="swot-list">
|
<ul class="swot-list">
|
||||||
<li>Extensive feature set may present learning curve for new users</li>
|
<li>
|
||||||
<li>Local deployment with multiple agents demands significant resources</li>
|
Extensive feature set may present learning curve for new users
|
||||||
<li>Advanced features require technical expertise beyond basic usage</li>
|
</li>
|
||||||
<li>Version 2.0 transition may create temporary compatibility concerns</li>
|
<li>
|
||||||
|
Local deployment with multiple agents demands significant
|
||||||
|
resources
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Advanced features require technical expertise beyond basic usage
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Version 2.0 transition may create temporary compatibility
|
||||||
|
concerns
|
||||||
|
</li>
|
||||||
<li>Integration complexity for custom MCP tools and workflows</li>
|
<li>Integration complexity for custom MCP tools and workflows</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@ -1010,28 +1147,43 @@
|
|||||||
<div class="section-header fade-in">
|
<div class="section-header fade-in">
|
||||||
<div class="section-tag">07 / Conclusion</div>
|
<div class="section-tag">07 / Conclusion</div>
|
||||||
<h2 class="section-title">Final Assessment</h2>
|
<h2 class="section-title">Final Assessment</h2>
|
||||||
<p class="section-desc">High confidence evaluation based on comprehensive analysis.</p>
|
<p class="section-desc">
|
||||||
|
High confidence evaluation based on comprehensive analysis.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-card fade-in">
|
<div class="summary-card fade-in">
|
||||||
<p class="summary-text">
|
<p class="summary-text">
|
||||||
DeerFlow represents a <strong>significant advancement</strong> in research automation, combining the power of multi-agent coordination, LLM-driven reasoning, and human-in-the-loop collaboration. Its modular architecture, deep tool integrations, and developer-friendly design make it a <strong>compelling choice</strong> for researchers and organizations seeking to accelerate complex workflows.
|
DeerFlow represents a <strong>significant advancement</strong> in
|
||||||
<br><br>
|
research automation, combining the power of multi-agent
|
||||||
With <strong>ByteDance backing</strong>, an active open-source community, and continuous feature development, DeerFlow is well-positioned to remain a leading framework in the deep research automation space. The upcoming DeerFlow 2.0 transition promises further enhancements to an already robust platform.
|
coordination, LLM-driven reasoning, and human-in-the-loop
|
||||||
|
collaboration. Its modular architecture, deep tool integrations, and
|
||||||
|
developer-friendly design make it a
|
||||||
|
<strong>compelling choice</strong> for researchers and organizations
|
||||||
|
seeking to accelerate complex workflows. <br /><br />
|
||||||
|
With <strong>ByteDance backing</strong>, an active open-source
|
||||||
|
community, and continuous feature development, DeerFlow is
|
||||||
|
well-positioned to remain a leading framework in the deep research
|
||||||
|
automation space. The upcoming DeerFlow 2.0 transition promises
|
||||||
|
further enhancements to an already robust platform.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<p class="footer-text">Research Report compiled on February 1, 2026 · GitHub Deep Research</p>
|
<p class="footer-text">
|
||||||
|
Research Report compiled on February 1, 2026 · GitHub Deep Research
|
||||||
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<a href="https://deerflow.tech" target="_blank" class="deerflow-badge">Created By Deerflow</a>
|
<a href="https://deerflow.tech" target="_blank" class="deerflow-badge"
|
||||||
|
>Created By Deerflow</a
|
||||||
|
>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Animated counter
|
// Animated counter
|
||||||
const animateCounter = (element) => {
|
const animateCounter = (element) => {
|
||||||
const target = parseInt(element.getAttribute('data-count'));
|
const target = parseInt(element.getAttribute("data-count"));
|
||||||
const duration = 2000;
|
const duration = 2000;
|
||||||
const step = target / (duration / 16);
|
const step = target / (duration / 16);
|
||||||
let current = 0;
|
let current = 0;
|
||||||
@ -1052,19 +1204,21 @@
|
|||||||
// Intersection Observer for animations
|
// Intersection Observer for animations
|
||||||
const observerOptions = {
|
const observerOptions = {
|
||||||
threshold: 0.1,
|
threshold: 0.1,
|
||||||
rootMargin: '0px 0px -50px 0px'
|
rootMargin: "0px 0px -50px 0px",
|
||||||
};
|
};
|
||||||
|
|
||||||
const observer = new IntersectionObserver((entries) => {
|
const observer = new IntersectionObserver((entries) => {
|
||||||
entries.forEach(entry => {
|
entries.forEach((entry) => {
|
||||||
if (entry.isIntersecting) {
|
if (entry.isIntersecting) {
|
||||||
entry.target.classList.add('visible');
|
entry.target.classList.add("visible");
|
||||||
|
|
||||||
// Trigger counters if this is the hero section
|
// Trigger counters if this is the hero section
|
||||||
const counters = entry.target.querySelectorAll('.stat-value[data-count]');
|
const counters = entry.target.querySelectorAll(
|
||||||
counters.forEach(counter => {
|
".stat-value[data-count]",
|
||||||
if (!counter.classList.contains('animated')) {
|
);
|
||||||
counter.classList.add('animated');
|
counters.forEach((counter) => {
|
||||||
|
if (!counter.classList.contains("animated")) {
|
||||||
|
counter.classList.add("animated");
|
||||||
animateCounter(counter);
|
animateCounter(counter);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -1073,42 +1227,49 @@
|
|||||||
}, observerOptions);
|
}, observerOptions);
|
||||||
|
|
||||||
// Observe all fade-in elements
|
// Observe all fade-in elements
|
||||||
document.querySelectorAll('.fade-in').forEach(el => observer.observe(el));
|
document
|
||||||
|
.querySelectorAll(".fade-in")
|
||||||
|
.forEach((el) => observer.observe(el));
|
||||||
|
|
||||||
// Observe hero for initial counter animation
|
// Observe hero for initial counter animation
|
||||||
const hero = document.querySelector('.hero');
|
const hero = document.querySelector(".hero");
|
||||||
if (hero) {
|
if (hero) {
|
||||||
const heroObserver = new IntersectionObserver((entries) => {
|
const heroObserver = new IntersectionObserver(
|
||||||
entries.forEach(entry => {
|
(entries) => {
|
||||||
|
entries.forEach((entry) => {
|
||||||
if (entry.isIntersecting) {
|
if (entry.isIntersecting) {
|
||||||
const counters = entry.target.querySelectorAll('.stat-value[data-count]');
|
const counters = entry.target.querySelectorAll(
|
||||||
counters.forEach(counter => {
|
".stat-value[data-count]",
|
||||||
if (!counter.classList.contains('animated')) {
|
);
|
||||||
counter.classList.add('animated');
|
counters.forEach((counter) => {
|
||||||
|
if (!counter.classList.contains("animated")) {
|
||||||
|
counter.classList.add("animated");
|
||||||
animateCounter(counter);
|
animateCounter(counter);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
heroObserver.unobserve(entry.target);
|
heroObserver.unobserve(entry.target);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, { threshold: 0.3 });
|
},
|
||||||
|
{ threshold: 0.3 },
|
||||||
|
);
|
||||||
|
|
||||||
heroObserver.observe(hero);
|
heroObserver.observe(hero);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Smooth scroll for navigation
|
// Smooth scroll for navigation
|
||||||
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) {
|
||||||
target.scrollIntoView({
|
target.scrollIntoView({
|
||||||
behavior: 'smooth',
|
behavior: "smooth",
|
||||||
block: 'start'
|
block: "start",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@ -112,7 +112,7 @@ 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 |
|
||||||
@ -129,7 +129,7 @@ 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 | ✅ | ❌ | ❌ |
|
||||||
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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({
|
||||||
|
|||||||
@ -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}`}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
@ -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={
|
||||||
|
|||||||
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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}`}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
};
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
};
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
};
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
};
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
};
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
};
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
};
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
};
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
};
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
};
|
||||||
|
|||||||
@ -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}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -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)}
|
||||||
|
|||||||
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -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" />
|
||||||
|
|||||||
@ -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)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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":
|
||||||
|
|||||||
@ -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/)**
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -288,7 +288,8 @@ export const enUS: Translations = {
|
|||||||
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",
|
||||||
},
|
},
|
||||||
|
|||||||
@ -308,7 +308,8 @@ export const zhCN: Translations = {
|
|||||||
"这条事实会立即从记忆中删除。此操作无法撤销。",
|
"这条事实会立即从记忆中删除。此操作无法撤销。",
|
||||||
factDeleteSuccess: "事实已删除",
|
factDeleteSuccess: "事实已删除",
|
||||||
noFacts: "还没有保存的事实。",
|
noFacts: "还没有保存的事实。",
|
||||||
summaryReadOnly: "摘要分区当前仍为只读。现在你可以清空全部记忆或删除单条事实。",
|
summaryReadOnly:
|
||||||
|
"摘要分区当前仍为只读。现在你可以清空全部记忆或删除单条事实。",
|
||||||
memoryFullyEmpty: "还没有保存任何记忆。",
|
memoryFullyEmpty: "还没有保存任何记忆。",
|
||||||
factPreviewLabel: "即将删除的事实",
|
factPreviewLabel: "即将删除的事实",
|
||||||
searchPlaceholder: "搜索记忆",
|
searchPlaceholder: "搜索记忆",
|
||||||
|
|||||||
@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>;
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -105,9 +105,7 @@ export function formatThreadAsJSON(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function sanitizeFilename(name: string): string {
|
function sanitizeFilename(name: string): string {
|
||||||
return (
|
return name.replace(/[^\p{L}\p{N}_\- ]/gu, "").trim() || "conversation";
|
||||||
name.replace(/[^\p{L}\p{N}_\- ]/gu, "").trim() || "conversation"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function downloadAsFile(
|
export function downloadAsFile(
|
||||||
|
|||||||
@ -429,7 +429,8 @@ export function useThreads(
|
|||||||
// Preserve prior semantics: if a non-positive limit is explicitly provided,
|
// Preserve prior semantics: if a non-positive limit is explicitly provided,
|
||||||
// delegate to a single search call with the original parameters.
|
// delegate to a single search call with the original parameters.
|
||||||
if (maxResults !== undefined && maxResults <= 0) {
|
if (maxResults !== undefined && maxResults <= 0) {
|
||||||
const response = await apiClient.threads.search<AgentThreadState>(params);
|
const response =
|
||||||
|
await apiClient.threads.search<AgentThreadState>(params);
|
||||||
return response as AgentThread[];
|
return response as AgentThread[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -33,9 +33,7 @@ async function readErrorDetail(
|
|||||||
response: Response,
|
response: Response,
|
||||||
fallback: string,
|
fallback: string,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const error = await response
|
const error = await response.json().catch(() => ({ detail: fallback }));
|
||||||
.json()
|
|
||||||
.catch(() => ({ detail: fallback }));
|
|
||||||
return error.detail ?? fallback;
|
return error.detail ?? fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user