we're basically done

This commit is contained in:
Maze Winther
2026-01-27 16:39:59 +01:00
parent af3100950e
commit 1f8391487e
70 changed files with 1230 additions and 699 deletions
+1 -1
View File
@@ -42,7 +42,7 @@ export default async function BlogPage() {
function BlogPostItem({ post }: { post: Post }) {
return (
<Link href={`/blog/${post.slug}`}>
<div className="flex h-auto w-full items-center justify-between py-6 opacity-100 transition-opacity hover:opacity-75">
<div className="flex h-auto w-full items-center justify-between py-6 opacity-100 hover:opacity-75">
<div className="flex flex-col gap-2">
<h2 className="text-xl font-semibold">{post.title}</h2>
<p className="text-muted-foreground">{post.description}</p>
+4 -6
View File
@@ -180,7 +180,7 @@ function AllContributorsSection({
href={contributor.html_url}
target="_blank"
rel="noopener noreferrer"
className="opacity-100 transition-opacity hover:opacity-70"
className="opacity-100 hover:opacity-70"
>
<div className="flex flex-col items-center gap-2 p-2">
<Avatar className="size-16">
@@ -214,7 +214,7 @@ function ExternalToolsSection() {
<p className="text-muted-foreground">Tools we use to build OpenCut</p>
</div>
<div className="mx-auto grid max-w-4xl grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3">
<div className="mx-auto grid max-w-4xl grid-cols-1 gap-6 sm:grid-cols-2">
{EXTERNAL_TOOLS.map((tool, index) => (
<Link
key={tool.url}
@@ -224,10 +224,8 @@ function ExternalToolsSection() {
style={{ animationDelay: `${index * 100}ms` }}
>
<Card className="h-full">
<CardContent className="flex h-full flex-col gap-4 p-6 text-center">
<div className="bg-muted/50 mx-auto flex size-12 items-center justify-center rounded-full">
<tool.icon className="size-6" />
</div>
<CardContent className="flex items-center justify-center h-full flex-col gap-4 p-6 text-center">
<tool.icon className="size-8" />
<div className="flex flex-1 flex-col gap-2">
<h3 className="text-lg font-semibold">{tool.name}</h3>
<p className="text-muted-foreground text-sm">
+9 -9
View File
@@ -15,6 +15,7 @@
--popover: hsl(0, 0%, 100%);
--popover-foreground: hsl(0 0% 2%);
--primary: hsl(203, 100%, 50%);
--primary-hover: hsl(203, 100%, 45%);
--primary-foreground: hsl(0, 0%, 100%);
--secondary: hsl(216, 13%, 92%);
--secondary-foreground: hsl(0 0% 2%);
@@ -44,9 +45,6 @@
--sidebar-ring: hsl(0 0% 16.9%);
--panel-background: hsl(216 13% 96%);
--panel-accent: hsl(216, 8%, 90%);
--radius: 1rem;
--sidebar: hsl(0 0% 98%);
}
.dark {
@@ -56,7 +54,8 @@
--card-foreground: hsl(0 0% 89%);
--popover: hsl(0 0% 14.9%);
--popover-foreground: hsl(0 0% 98%);
--primary: hsl(205, 84%, 53%);
--primary: hsl(203, 100%, 50%);
--primary-hover: hsl(203, 100%, 45%);
--primary-foreground: hsl(0 0% 9%);
--secondary: hsl(0 0% 14.9%);
--secondary-foreground: hsl(0 0% 98%);
@@ -124,15 +123,16 @@
--font-sans: var(--font-inter), sans-serif;
/* Font sizes */
--text-base: 0.95rem;
--text-xl: 1.20rem;
--text-base: 0.92rem;
--text-base--line-height: calc(1.5 / 0.95);
--text-xs: 0.75rem;
--text-xs--line-height: calc(1 / 0.8);
/* Border radius */
--radius-lg: var(--radius);
--radius-md: calc(var(--radius) - 2px);
--radius-sm: calc(var(--radius) - 8px);
--radius-lg: 0.82rem;
--radius-md: 0.65rem;
--radius-sm: 0.35rem;
/* Palette mapped to root design tokens */
--color-background: var(--background);
@@ -146,7 +146,7 @@
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-primary-hover: var(--primary-hover);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
+50 -24
View File
@@ -110,7 +110,11 @@ export default function ProjectsPage() {
}
>
{projectsToDisplay.map((project) => (
<ProjectItem key={project.id} project={project} />
<ProjectItem
key={project.id}
project={project}
allProjectIds={projectsToDisplay.map((p) => p.id)}
/>
))}
</div>
)}
@@ -144,10 +148,10 @@ function ProjectsHeader() {
</BreadcrumbList>
</Breadcrumb>
<div className="hidden md:flex rounded-full border p-1 h-10">
<div className="hidden md:flex rounded-md border p-1 h-10">
<button
type="button"
className={`p-2 rounded-full cursor-pointer ${isHydrated && viewMode === "grid" ? "bg-accent/75" : ""}`}
className={`p-2 rounded-sm cursor-pointer ${isHydrated && viewMode === "grid" ? "bg-accent/75" : ""}`}
onClick={() => setViewMode({ viewMode: "grid" })}
onKeyDown={(event) =>
event.key === "Enter" && setViewMode({ viewMode: "grid" })
@@ -159,7 +163,7 @@ function ProjectsHeader() {
</button>
<button
type="button"
className={`p-2 rounded-full cursor-pointer ${isHydrated && viewMode === "list" ? "bg-accent/75" : ""}`}
className={`p-2 rounded-sm cursor-pointer ${isHydrated && viewMode === "list" ? "bg-accent/75" : ""}`}
onClick={() => setViewMode({ viewMode: "list" })}
onKeyDown={(event) =>
event.key === "Enter" && setViewMode({ viewMode: "list" })
@@ -219,7 +223,7 @@ function ProjectsToolbar({ projectIds }: { projectIds: string[] }) {
};
return (
<div className="sticky top-16 z-10 flex items-center justify-between px-6 h-14 bg-background">
<div className="sticky top-16 z-10 flex items-center justify-between px-6 h-14 pt-2 bg-background">
<div className="flex items-center gap-2">
<Label
className="flex items-center gap-3 cursor-pointer px-2"
@@ -298,7 +302,7 @@ function ProjectsToolbar({ projectIds }: { projectIds: string[] }) {
</Button>
</div>
</div>
{selectedProjectCount > 1 ? <ProjectActions /> : null}
{selectedProjectCount > 0 ? <ProjectActions /> : null}
</div>
);
}
@@ -518,20 +522,24 @@ function NewProjectButton() {
return (
<Button
variant="primary"
size="lg"
className="flex px-5 md:px-6"
onClick={handleCreateProject}
>
<HugeiconsIcon icon={PlusSignIcon} />
<span className="text-sm font-medium hidden md:block">New project</span>
<span className="text-sm font-medium block md:hidden">New</span>
</Button>
);
}
function ProjectItem({ project }: { project: TProjectMetadata }) {
const { selectedProjectIds, viewMode, setProjectSelected } =
function ProjectItem({
project,
allProjectIds,
}: {
project: TProjectMetadata;
allProjectIds: string[];
}) {
const { selectedProjectIds, viewMode, setProjectSelected, selectProjectRange } =
useProjectsStore();
const selectedProjectIdSet = new Set(selectedProjectIds);
const isSelected = selectedProjectIdSet.has(project.id);
@@ -541,6 +549,20 @@ function ProjectItem({ project }: { project: TProjectMetadata }) {
const isMultiSelect = selectedProjectCount > 1;
const isGridView = viewMode === "grid";
const handleCheckboxChange = ({
checked,
shiftKey,
}: {
checked: boolean;
shiftKey: boolean;
}) => {
if (shiftKey && checked) {
selectProjectRange({ projectId: project.id, allProjectIds });
return;
}
setProjectSelected({ projectId: project.id, isSelected: checked });
};
const gridContent = (
<Card className="bg-background overflow-hidden border-none p-0">
<div className="bg-muted relative aspect-video">
@@ -567,7 +589,7 @@ function ProjectItem({ project }: { project: TProjectMetadata }) {
</div>
<CardContent className="flex flex-col gap-2 px-0 pt-4">
<h3 className="group-hover:text-foreground/90 line-clamp-2 text-sm leading-snug font-medium transition-colors">
<h3 className="group-hover:text-foreground/90 line-clamp-2 text-sm leading-snug font-medium">
{project.name}
</h3>
<div className="text-muted-foreground flex items-center gap-1.5 text-sm">
@@ -595,7 +617,7 @@ function ProjectItem({ project }: { project: TProjectMetadata }) {
)}
</div>
<h3 className="group-hover:text-foreground/90 text-sm font-medium truncate flex-1 min-w-0 transition-colors">
<h3 className="group-hover:text-foreground/90 text-sm font-medium truncate flex-1 min-w-0">
{project.name}
</h3>
@@ -617,12 +639,14 @@ function ProjectItem({ project }: { project: TProjectMetadata }) {
>
<Checkbox
checked={isSelected}
onCheckedChange={(checked) =>
setProjectSelected({
projectId: project.id,
isSelected: checked === true,
})
}
onMouseDown={(event) => event.preventDefault()}
onClick={(event) => {
handleCheckboxChange({
checked: !isSelected,
shiftKey: event.shiftKey,
});
}}
onCheckedChange={() => {}}
className="size-5 shrink-0"
/>
@@ -657,12 +681,14 @@ function ProjectItem({ project }: { project: TProjectMetadata }) {
<>
<Checkbox
checked={isSelected}
onCheckedChange={(checked) =>
setProjectSelected({
projectId: project.id,
isSelected: checked === true,
})
}
onMouseDown={(event) => event.preventDefault()}
onClick={(event) => {
handleCheckboxChange({
checked: !isSelected,
shiftKey: event.shiftKey,
});
}}
onCheckedChange={() => {}}
className={`absolute z-10 size-5 top-3 left-3 ${
isSelected || isDropdownOpen
? "opacity-100"
+41 -1
View File
@@ -10,6 +10,7 @@ interface ProjectsState {
sortOrder: "asc" | "desc";
viewMode: ProjectsViewMode;
selectedProjectIds: string[];
lastSelectedProjectId: string | null;
isHydrated: boolean;
setIsHydrated: ({ isHydrated }: { isHydrated: boolean }) => void;
setSearchQuery: ({ query }: { query: string }) => void;
@@ -26,6 +27,13 @@ interface ProjectsState {
projectId: string;
isSelected: boolean;
}) => void;
selectProjectRange: ({
projectId,
allProjectIds,
}: {
projectId: string;
allProjectIds: string[];
}) => void;
}
const getNextSelectedProjectIds = ({
@@ -56,6 +64,7 @@ export const useProjectsStore = create<ProjectsState>()(
sortOrder: "desc",
viewMode: "grid",
selectedProjectIds: [],
lastSelectedProjectId: null,
isHydrated: false,
setIsHydrated: ({ isHydrated }) => set({ isHydrated }),
setSearchQuery: ({ query }) => set({ searchQuery: query }),
@@ -68,7 +77,8 @@ export const useProjectsStore = create<ProjectsState>()(
setViewMode: ({ viewMode }) => set({ viewMode }),
setSelectedProjects: ({ projectIds }) =>
set({ selectedProjectIds: projectIds }),
clearSelectedProjects: () => set({ selectedProjectIds: [] }),
clearSelectedProjects: () =>
set({ selectedProjectIds: [], lastSelectedProjectId: null }),
setProjectSelected: ({ projectId, isSelected }) =>
set((state) => ({
selectedProjectIds: getNextSelectedProjectIds({
@@ -76,7 +86,37 @@ export const useProjectsStore = create<ProjectsState>()(
projectId,
isSelected,
}),
lastSelectedProjectId: isSelected ? projectId : state.lastSelectedProjectId,
})),
selectProjectRange: ({ projectId, allProjectIds }) =>
set((state) => {
const anchorId = state.lastSelectedProjectId;
if (!anchorId) {
return {
selectedProjectIds: [projectId],
lastSelectedProjectId: projectId,
};
}
const anchorIndex = allProjectIds.indexOf(anchorId);
const targetIndex = allProjectIds.indexOf(projectId);
if (anchorIndex === -1 || targetIndex === -1) {
return {
selectedProjectIds: [projectId],
lastSelectedProjectId: projectId,
};
}
const startIndex = Math.min(anchorIndex, targetIndex);
const endIndex = Math.max(anchorIndex, targetIndex);
const rangeIds = allProjectIds.slice(startIndex, endIndex + 1);
const merged = new Set([...state.selectedProjectIds, ...rangeIds]);
return {
selectedProjectIds: Array.from(merged),
};
}),
}),
{
name: "projects-view-mode",