Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ README.md
Dockerfile
docker-compose.yml
node_modules/ # Installed inside container
nginx/ # We'll create this directory soon
nginx/ # We'll create this directory soon
landing/ # Standalone static homepage — built & deployed separately
67 changes: 67 additions & 0 deletions .github/workflows/deploy-landing-release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: Deploy Landing (Release)

on:
push:
branches: [release]
paths:
- "landing/**"
# Allow redeploying the homepage on demand without a commit.
workflow_dispatch:

permissions:
contents: read
packages: write

jobs:
deploy-landing:
runs-on: ubuntu-latest
env:
IMAGE_NAME: ghcr.io/lycoon/scriptio-landing
steps:
- uses: actions/checkout@v6

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Set commit SHA
run: echo "COMMIT_SHA=$(git rev-parse --short HEAD)" >> $GITHUB_ENV

- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push landing image
uses: docker/build-push-action@v6
with:
context: ./landing
push: true
tags: |
${{ env.IMAGE_NAME }}:latest
${{ env.IMAGE_NAME }}:${{ env.COMMIT_SHA }}

- name: Copy docker-compose to VPS
uses: appleboy/scp-action@master
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USERNAME }}
key: ${{ secrets.SSH_SECRET }}
port: ${{ secrets.SSH_PORT }}
source: "docker-compose.yml"
target: ${{ secrets.APP_PATH }}

- name: Deploy landing via SSH
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USERNAME }}
key: ${{ secrets.SSH_SECRET }}
port: ${{ secrets.SSH_PORT }}
script: |
set -e
cd ${{ secrets.APP_PATH }}
docker compose pull landing-prod
docker compose --env-file .env.prod --profile prod up -d --no-deps --force-recreate landing-prod
docker image prune -f
67 changes: 67 additions & 0 deletions .github/workflows/deploy-landing-staging.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: Deploy Landing (Staging)

on:
push:
branches: [staging]
paths:
- "landing/**"
# Allow redeploying the homepage on demand without a commit.
workflow_dispatch:

permissions:
contents: read
packages: write

jobs:
deploy-landing:
runs-on: ubuntu-latest
env:
IMAGE_NAME: ghcr.io/lycoon/scriptio-landing
steps:
- uses: actions/checkout@v6

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Set commit SHA
run: echo "COMMIT_SHA=$(git rev-parse --short HEAD)" >> $GITHUB_ENV

- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push landing image
uses: docker/build-push-action@v6
with:
context: ./landing
push: true
tags: |
${{ env.IMAGE_NAME }}:staging
${{ env.IMAGE_NAME }}:staging-${{ env.COMMIT_SHA }}

- name: Copy docker-compose to VPS
uses: appleboy/scp-action@master
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USERNAME }}
key: ${{ secrets.SSH_SECRET }}
port: ${{ secrets.SSH_PORT }}
source: "docker-compose.yml"
target: ${{ secrets.APP_PATH }}

- name: Deploy landing via SSH
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USERNAME }}
key: ${{ secrets.SSH_SECRET }}
port: ${{ secrets.SSH_PORT }}
script: |
set -e
cd ${{ secrets.APP_PATH }}
docker compose pull landing-staging
docker compose --env-file .env.staging --profile staging up -d --no-deps --force-recreate landing-staging
docker image prune -f
4 changes: 4 additions & 0 deletions .github/workflows/deploy-release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ name: Deploy Release
on:
push:
branches: [release]
# Homepage-only changes deploy via deploy-landing-release.yaml and must
# not trigger the app image rebuild + desktop publish pipeline.
paths-ignore:
- "landing/**"

permissions:
contents: write
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/deploy-staging.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ name: Deploy Staging
on:
push:
branches: [staging]
# Homepage-only changes deploy via deploy-landing-staging.yaml and must
# not trigger the app image rebuild + desktop publish pipeline.
paths-ignore:
- "landing/**"

permissions:
contents: read
Expand Down
36 changes: 32 additions & 4 deletions components/board/BoardCanvas.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -652,15 +652,43 @@
user-select: text;
}

/* Enlarge both drag targets into comfortable ~40px touch areas. The visible
* mark (resize chevron / connection dot) stays small in the corner; the rest
* of the box is a transparent, tappable margin extending into the card. */
.card_resize_handle {
opacity: 1;
width: 28px;
height: 28px;
width: 40px;
height: 40px;
}

/* The whole 40px box is the touch target, but transparent — the big white
* radial-gradient that fills it on desktop is dropped here so it doesn't
* read as a white blob spilling over the card. The visible node is drawn by
* ::before, kept small and pinned to the box's top-right corner. */
.connection_handle {
opacity: 1;
width: 28px;
height: 28px;
width: 40px;
height: 40px;
background: none;
}

.connection_handle::before {
top: 4px;
right: 4px;
left: auto;
width: 18px;
height: 18px;
transform: none;
background: white;
border: 1.5px solid rgba(0, 0, 0, 0.25);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25);
opacity: 1;
}

/* No hover on touch — keep the node from scaling/relayering if a pointer
* hovers it (e.g. testing at mobile width on desktop). */
.connection_handle:hover {
transform: none;
background: none;
}
}
79 changes: 58 additions & 21 deletions components/board/BoardCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@
import { useContext, useRef, useState, useCallback, useEffect, useMemo } from "react";
import { ProjectContext } from "@src/context/ProjectContext";
import { UserContext } from "@src/context/UserContext";
import { BoardCardData, BoardArrowData } from "@src/lib/project/project-state";
import { BoardCardData, BoardArrowData, TimelineLayer } from "@src/lib/project/project-state";
import BoardCard from "./BoardCard";
import {
ContextMenuItem,
ContextMenuSeparator,
ContextMenuColorRow,
ContextMenuSubmenu,
} from "@components/utils/ContextMenu";
import styles from "./BoardCanvas.module.css";
import { v7 as uuidv7 } from "uuid";
import { Trash2, Plus, Minus, Copy, ListTree, Mic, Square, Image as ImageIcon } from "lucide-react";
import { Trash2, Plus, Minus, Copy, ListTree, Layers, Mic, Square, Image as ImageIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { DEFAULT_ITEM_COLORS } from "@src/lib/utils/colors";
import { importImageFile, importAudioFile, syncAssetToCloud } from "@src/lib/assets/asset-store";
Expand Down Expand Up @@ -43,7 +44,7 @@ function formatRecordingTime(seconds: number): string {
}

const BoardCanvas =({ isVisible, docId }: { isVisible: boolean; docId: string }) => {
const { projectId, repository, isYjsReady, isReadOnly, boardFocusCardId, setBoardFocusCardId } =
const { projectId, repository, isYjsReady, isReadOnly, boardFocusCardId, setBoardFocusCardId, timelineLayers } =
useContext(ProjectContext);
const { updateContextMenu } = useContext(UserContext);
const t = useTranslations("board");
Expand Down Expand Up @@ -923,21 +924,44 @@ const BoardCanvas =({ isVisible, docId }: { isVisible: boolean; docId: string })
[cards, saveCards],
);

// Send card to the Timeline
// Send card to the Timeline — to a specific layer when `layerId` is given,
// otherwise to the default (first root) lane.
const handleSendToTimeline = useCallback(
(card: BoardCardData) => {
repository?.appendTimelineClip({
source: "card",
refDocId: docId,
refId: card.id,
title: card.title,
preview: card.description,
color: card.color,
});
(card: BoardCardData, layerId?: string) => {
repository?.appendTimelineClip(
{
source: "card",
refDocId: docId,
refId: card.id,
title: card.title,
preview: card.description,
color: card.color,
},
undefined,
layerId,
);
},
[repository, docId],
);

// Timeline layers flattened into display order (depth-annotated), mirroring
// the Timeline panel's tree so the "Send to timeline" submenu matches it.
const orderedLayers = useMemo(() => {
const out: { layer: TimelineLayer; depth: number }[] = [];
const childrenOf = (parentId: string | null) =>
Object.values(timelineLayers)
.filter((l) => (l.parentId ?? null) === parentId)
.sort((a, b) => a.order - b.order);
const walk = (parentId: string | null, depth: number) => {
for (const layer of childrenOf(parentId)) {
out.push({ layer, depth });
walk(layer.id, depth + 1);
}
};
walk(null, 0);
return out;
}, [timelineLayers]);

// Delete arrow
const handleDeleteArrow = useCallback(
(id: string) => {
Expand Down Expand Up @@ -971,13 +995,26 @@ const BoardCanvas =({ isVisible, docId }: { isVisible: boolean; docId: string })
text={t("duplicate")}
action={() => handleDuplicateCard(card)}
/>
{(card.type ?? "text") === "text" && (
<ContextMenuItem
icon={ListTree}
text={t("sendToTimeline")}
action={() => handleSendToTimeline(card)}
/>
)}
{(card.type ?? "text") === "text" &&
(orderedLayers.length > 0 ? (
<ContextMenuSubmenu icon={ListTree} text={t("sendToTimeline")}>
{orderedLayers.map(({ layer, depth }) => (
<ContextMenuItem
key={layer.id}
icon={Layers}
indent={depth * 14}
text={layer.name || t("untitled")}
action={() => handleSendToTimeline(card, layer.id)}
/>
))}
</ContextMenuSubmenu>
) : (
<ContextMenuItem
icon={ListTree}
text={t("sendToTimeline")}
action={() => handleSendToTimeline(card)}
/>
))}
<ContextMenuItem
icon={Trash2}
text={t("delete")}
Expand All @@ -987,7 +1024,7 @@ const BoardCanvas =({ isVisible, docId }: { isVisible: boolean; docId: string })
),
});
},
[updateContextMenu, t, handleChangeCardColor, handleDuplicateCard, handleSendToTimeline, handleDeleteCard],
[updateContextMenu, t, handleChangeCardColor, handleDuplicateCard, handleSendToTimeline, handleDeleteCard, orderedLayers],
);

// Open the shared context-menu host for an arrow.
Expand Down
Loading
Loading