Merge pull request #2 from hex248/development

Made better use of --personality colour
This commit is contained in:
Oliver Bryan
2026-01-27 12:45:34 +00:00
committed by GitHub
18 changed files with 198 additions and 52 deletions

View File

@@ -1,5 +1,16 @@
import "dotenv/config"; import "dotenv/config";
import { Issue, Organisation, OrganisationMember, Project, User } from "@sprint/shared"; import {
DEFAULT_ISSUE_TYPES,
DEFAULT_STATUS_COLOURS,
Issue,
IssueAssignee,
IssueComment,
Organisation,
OrganisationMember,
Project,
Sprint,
User,
} from "@sprint/shared";
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
import { drizzle } from "drizzle-orm/node-postgres"; import { drizzle } from "drizzle-orm/node-postgres";
@@ -66,6 +77,24 @@ const issues = [
{ title: "Add batch processing", description: "Need to process large datasets efficiently." }, { title: "Add batch processing", description: "Need to process large datasets efficiently." },
]; ];
const issueStatuses = Object.keys(DEFAULT_STATUS_COLOURS);
const issueTypes = Object.keys(DEFAULT_ISSUE_TYPES);
const issueComments = [
"started looking into this, will share updates soon",
"i can reproduce this on staging",
"adding details in the description",
"should be a small fix, pairing with u2",
"blocked on api response shape",
"added logs, issue still happening",
"fix is ready for review",
"can we confirm expected behavior?",
"this seems related to recent deploy",
"i will take this one",
"qa verified on latest build",
"needs product input before proceeding",
];
const passwordHash = await hashPassword("a"); const passwordHash = await hashPassword("a");
const users = [ const users = [
{ name: "user 1", username: "u1", passwordHash, avatarURL: null }, { name: "user 1", username: "u1", passwordHash, avatarURL: null },
@@ -146,14 +175,48 @@ async function seed() {
console.log(`created ${projects.length} projects`); console.log(`created ${projects.length} projects`);
// create 3-6 issues per project console.log("creating sprints...");
const sprintValues = [];
const now = new Date();
for (const project of projects) {
sprintValues.push(
{
projectId: project.id,
name: "Sprint 1",
color: "#3b82f6",
startDate: new Date(now.getTime() - 14 * 24 * 60 * 60 * 1000),
endDate: new Date(now.getTime() - 1 * 24 * 60 * 60 * 1000),
},
{
projectId: project.id,
name: "Sprint 2",
color: "#22c55e",
startDate: new Date(now.getTime()),
endDate: new Date(now.getTime() + 13 * 24 * 60 * 60 * 1000),
},
);
}
const createdSprints = await db.insert(Sprint).values(sprintValues).returning();
const sprintsByProject = new Map<number, (typeof createdSprints)[number][]>();
for (const sprint of createdSprints) {
const list = sprintsByProject.get(sprint.projectId) ?? [];
list.push(sprint);
sprintsByProject.set(sprint.projectId, list);
}
console.log(`created ${createdSprints.length} sprints`);
// create 6-12 issues per project
console.log("creating issues..."); console.log("creating issues...");
const allUsers = [u1, u2]; const allUsers = [u1, u2];
const issueValues = []; const issueValues = [];
let issueIndex = 0; let issueIndex = 0;
for (const project of projects) { for (const project of projects) {
const numIssues = Math.floor(Math.random() * 4) + 3; // 3-6 issues const numIssues = Math.floor(Math.random() * 7) + 6; // 6-12 issues
for (let i = 1; i <= numIssues; i++) { for (let i = 1; i <= numIssues; i++) {
const creator = allUsers[Math.floor(Math.random() * allUsers.length)]; const creator = allUsers[Math.floor(Math.random() * allUsers.length)];
if (!creator) { if (!creator) {
@@ -164,20 +227,89 @@ async function seed() {
throw new Error("failed to select issue"); throw new Error("failed to select issue");
} }
issueIndex++; issueIndex++;
const status = issueStatuses[Math.floor(Math.random() * issueStatuses.length)];
const type = issueTypes[Math.floor(Math.random() * issueTypes.length)];
if (!status || !type) {
throw new Error("failed to select issue status or type");
}
const projectSprints = sprintsByProject.get(project.id);
if (!projectSprints || projectSprints.length === 0) {
throw new Error("failed to select project sprint");
}
const sprint = projectSprints[Math.floor(Math.random() * projectSprints.length)];
if (!sprint) {
throw new Error("failed to select sprint");
}
issueValues.push({ issueValues.push({
projectId: project.id, projectId: project.id,
number: i, number: i,
title: issue.title, title: issue.title,
description: issue.description, description: issue.description,
status,
type,
creatorId: creator.id, creatorId: creator.id,
sprintId: sprint.id,
}); });
} }
} }
await db.insert(Issue).values(issueValues); const createdIssues = await db.insert(Issue).values(issueValues).returning();
console.log(`created ${issueValues.length} issues`); console.log(`created ${createdIssues.length} issues`);
console.log("creating issue assignees...");
const assigneeValues = [];
for (const issue of createdIssues) {
const assigneeCount = Math.floor(Math.random() * 3);
const picked = new Set<number>();
for (let i = 0; i < assigneeCount; i++) {
const assignee = usersDB[Math.floor(Math.random() * usersDB.length)];
if (!assignee) {
throw new Error("failed to select issue assignee");
}
if (picked.has(assignee.id)) {
continue;
}
picked.add(assignee.id);
assigneeValues.push({
issueId: issue.id,
userId: assignee.id,
});
}
}
if (assigneeValues.length > 0) {
await db.insert(IssueAssignee).values(assigneeValues);
}
console.log(`created ${assigneeValues.length} issue assignees`);
console.log("creating issue comments...");
const commentValues = [];
for (const issue of createdIssues) {
const commentCount = Math.floor(Math.random() * 3);
for (let i = 0; i < commentCount; i++) {
const commenter = usersDB[Math.floor(Math.random() * usersDB.length)];
const comment = issueComments[Math.floor(Math.random() * issueComments.length)];
if (!commenter || !comment) {
throw new Error("failed to select issue comment data");
}
commentValues.push({
issueId: issue.id,
userId: commenter.id,
body: comment,
});
}
}
if (commentValues.length > 0) {
await db.insert(IssueComment).values(commentValues);
}
console.log(`created ${commentValues.length} issue comments`);
console.log("database seeding complete"); console.log("database seeding complete");
console.log("\ndemo accounts (password: a):"); console.log("\ndemo accounts (password: a):");

View File

@@ -40,7 +40,7 @@ const main = async () => {
"/user/by-username": withGlobal(withAuth(routes.userByUsername)), "/user/by-username": withGlobal(withAuth(routes.userByUsername)),
"/user/update": withGlobal(withAuth(withCSRF(routes.userUpdate))), "/user/update": withGlobal(withAuth(withCSRF(routes.userUpdate))),
"/user/upload-avatar": withGlobal(withAuth(withCSRF(routes.userUploadAvatar))), "/user/upload-avatar": withGlobal(routes.userUploadAvatar),
"/issue/create": withGlobal(withAuth(withCSRF(routes.issueCreate))), "/issue/create": withGlobal(withAuth(withCSRF(routes.issueCreate))),
"/issue/by-id": withGlobal(withAuth(routes.issueById)), "/issue/by-id": withGlobal(withAuth(routes.issueById)),

View File

@@ -66,10 +66,10 @@
--muted-foreground: oklch(0.556 0 0); --muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0); --accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0); --accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325); --destructive: oklch(61.275% 0.20731 24.986);
--border: oklch(73.802% 0.00008 271.152); --border: oklch(73.802% 0.00008 271.152);
--input: oklch(0.922 0 0); --input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0); --ring: var(--personality);
--chart-1: oklch(0.646 0.222 41.116); --chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704); --chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392); --chart-3: oklch(0.398 0.07 227.392);
@@ -105,7 +105,7 @@
--destructive: oklch(0.704 0.191 22.216); --destructive: oklch(0.704 0.191 22.216);
--border: oklch(100% 0.00011 271.152 / 0.22); --border: oklch(100% 0.00011 271.152 / 0.22);
--input: oklch(1 0 0 / 15%); --input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0); --ring: var(--personality);
--chart-1: oklch(0.488 0.243 264.376); --chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48); --chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08); --chart-3: oklch(0.769 0.188 70.08);
@@ -132,6 +132,10 @@
background-color: var(--personality); background-color: var(--personality);
color: var(--background); color: var(--background);
} }
a:focus-visible {
outline: 1px solid var(--personality);
outline-offset: 2px;
}
} }
* { * {

View File

@@ -49,6 +49,7 @@ export default function Avatar({
size, size,
textClass = "text-xs", textClass = "text-xs",
strong = false, strong = false,
skipOrgCheck = false,
className, className,
}: { }: {
avatarURL?: string | null; avatarURL?: string | null;
@@ -57,6 +58,7 @@ export default function Avatar({
size?: number; size?: number;
textClass?: string; textClass?: string;
strong?: boolean; strong?: boolean;
skipOrgCheck?: boolean;
className?: string; className?: string;
}) { }) {
// if the username matches the authed user, use their avatarURL and name (avoid stale data) // if the username matches the authed user, use their avatarURL and name (avoid stale data)
@@ -69,13 +71,15 @@ export default function Avatar({
? FALLBACK_COLOURS[hashStringToIndex(username, FALLBACK_COLOURS.length)] ? FALLBACK_COLOURS[hashStringToIndex(username, FALLBACK_COLOURS.length)]
: "bg-muted"; : "bg-muted";
const showAvatar = skipOrgCheck || selectedOrganisation?.Organisation.features.userAvatars;
return ( return (
<div <div
className={cn( className={cn(
"flex items-center justify-center rounded-full", "flex items-center justify-center rounded-full",
"text-white font-medium select-none", "text-white font-medium select-none",
name && "border", name && "border",
(!avatarURL || !selectedOrganisation?.Organisation.features.userAvatars) && backgroundClass, (!avatarURL || !showAvatar) && backgroundClass,
"transition-colors", "transition-colors",
`w-${size || 6}`, `w-${size || 6}`,
@@ -83,7 +87,7 @@ export default function Avatar({
className, className,
)} )}
> >
{selectedOrganisation?.Organisation.features.userAvatars && avatarURL ? ( {showAvatar && avatarURL ? (
<img <img
src={avatarURL} src={avatarURL}
alt="Avatar" alt="Avatar"

View File

@@ -118,7 +118,6 @@ export function IssueComments({ issueId, className }: { issueId: number; classNa
</div> </div>
{isAuthor ? ( {isAuthor ? (
<IconButton <IconButton
variant="ghost"
onClick={() => handleDelete(comment)} onClick={() => handleDelete(comment)}
disabled={deletingId === comment.Comment.id} disabled={deletingId === comment.Comment.id}
title="Delete comment" title="Delete comment"

View File

@@ -454,7 +454,7 @@ export function IssueDetails({
/> />
</div> </div>
</div> </div>
{organisation?.Organisation.features.description && {organisation?.Organisation.features.issueDescriptions &&
(description || isEditingDescription ? ( (description || isEditingDescription ? (
<Textarea <Textarea
ref={descriptionRef} ref={descriptionRef}

View File

@@ -147,32 +147,33 @@ export function IssuesTable({
e.preventDefault(); e.preventDefault();
}; };
const showId = columns.id == null || columns.id === true;
const showTitle = columns.title == null || columns.title === true;
const showDescription = columns.description == null || columns.description === true;
const showAssignee = columns.assignee == null || columns.assignee === true;
return ( return (
<Table className={cn("table-fixed", className)}> <Table className={cn("table-fixed", className)}>
<TableHeader> <TableHeader>
<TableRow hoverEffect={false} className="bg-secondary"> <TableRow hoverEffect={false} className="bg-secondary">
{(columns.id == null || columns.id === true) && ( {showId && (
<TableHead className="text-right w-10 border-r text-xs font-medium text-muted-foreground"> <TableHead className="text-right w-10 border-r text-xs font-medium text-muted-foreground">
ID ID
</TableHead> </TableHead>
)} )}
{(columns.title == null || columns.title === true) && ( {showTitle && <TableHead className="text-xs font-medium text-muted-foreground">Title</TableHead>}
<TableHead className="text-xs font-medium text-muted-foreground">Title</TableHead> {showDescription && (
)}
{(columns.description == null || columns.description === true) && (
<TableHead className="text-xs font-medium text-muted-foreground">Description</TableHead> <TableHead className="text-xs font-medium text-muted-foreground">Description</TableHead>
)} )}
{/* below is kept blank to fill the space, used as the "Assignee" column */} {/* below is kept blank to fill the space, used as the "Assignee" column */}
{(columns.assignee == null || columns.assignee === true) && ( {showAssignee && <TableHead className="w-[1%]"></TableHead>}
<TableHead className="w-[1%]"></TableHead>
)}
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{issues.map((issueData) => ( {issues.map((issueData) => (
<TableRow <TableRow
key={issueData.Issue.id} key={issueData.Issue.id}
className="cursor-pointer max-w-full" className={cn("cursor-pointer max-w-full")}
onClick={() => { onClick={() => {
if (issueData.Issue.id === selectedIssueId) { if (issueData.Issue.id === selectedIssueId) {
selectIssue(null); selectIssue(null);
@@ -181,8 +182,13 @@ export function IssuesTable({
selectIssue(issueData); selectIssue(issueData);
}} }}
> >
{(columns.id == null || columns.id === true) && ( {showId && (
<TableCell className="font-medium border-r text-right p-0"> <TableCell
className={cn(
"font-medium border-r text-right p-0",
issueData.Issue.id === selectedIssueId && "shadow-[inset_2px_0_0_0_var(--personality)]",
)}
>
<a <a
href={getIssueUrl(issueData.Issue.number)} href={getIssueUrl(issueData.Issue.number)}
onClick={handleLinkClick} onClick={handleLinkClick}
@@ -192,8 +198,15 @@ export function IssuesTable({
</a> </a>
</TableCell> </TableCell>
)} )}
{(columns.title == null || columns.title === true) && ( {showTitle && (
<TableCell className="min-w-0 p-0"> <TableCell
className={cn(
"min-w-0 p-0",
!showId &&
issueData.Issue.id === selectedIssueId &&
"shadow-[inset_2px_0_0_0_var(--personality)]",
)}
>
<a <a
href={getIssueUrl(issueData.Issue.number)} href={getIssueUrl(issueData.Issue.number)}
onClick={handleLinkClick} onClick={handleLinkClick}
@@ -215,7 +228,7 @@ export function IssuesTable({
</a> </a>
</TableCell> </TableCell>
)} )}
{(columns.description == null || columns.description === true) && ( {showDescription && (
<TableCell className="overflow-hidden p-0"> <TableCell className="overflow-hidden p-0">
<a <a
href={getIssueUrl(issueData.Issue.number)} href={getIssueUrl(issueData.Issue.number)}
@@ -226,7 +239,7 @@ export function IssuesTable({
</a> </a>
</TableCell> </TableCell>
)} )}
{(columns.assignee == null || columns.assignee === true) && ( {showAssignee && (
<TableCell className="h-[32px] p-0"> <TableCell className="h-[32px] p-0">
<a <a
href={getIssueUrl(issueData.Issue.number)} href={getIssueUrl(issueData.Issue.number)}

View File

@@ -222,6 +222,7 @@ export default function LogInForm() {
username={username || undefined} username={username || undefined}
avatarURL={avatarURL} avatarURL={avatarURL}
onAvatarUploaded={setAvatarUrl} onAvatarUploaded={setAvatarUrl}
skipOrgCheck
className="mb-2" className="mb-2"
/> />
{avatarURL && ( {avatarURL && (

View File

@@ -724,7 +724,7 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
}); });
}} }}
> >
<Icon icon="trash" className="size-4" /> <Icon icon="trash" className="size-4" color="white" />
Delete Delete
</Button> </Button>
)} )}
@@ -872,7 +872,7 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
}); });
}} }}
> >
<Icon icon="trash" className="size-4" /> <Icon icon="trash" className="size-4" color="white" />
Delete Delete
</Button> </Button>
)} )}
@@ -951,7 +951,7 @@ function Organisations({ trigger }: { trigger?: ReactNode }) {
}} }}
className="hover:bg-destructive/10" className="hover:bg-destructive/10"
> >
<Icon icon="trash" className="size-4" /> <Icon icon="trash" className="size-4" color="white" />
Delete Delete
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>

View File

@@ -75,11 +75,9 @@ export function TimerControls({
<div className={cn("ml-auto flex items-center", isCompact ? "gap-1" : "gap-2")}> <div className={cn("ml-auto flex items-center", isCompact ? "gap-1" : "gap-2")}>
<IconButton <IconButton
size={"sm"} size={"sm"}
variant="dummy"
aria-label={running ? "Pause timer" : "Resume timer"} aria-label={running ? "Pause timer" : "Resume timer"}
disabled={disabled} disabled={disabled}
onClick={handleToggle} onClick={handleToggle}
className={"hover:opacity-70"}
> >
{running ? ( {running ? (
<Icon icon="pause" size={isCompact ? 14 : 16} /> <Icon icon="pause" size={isCompact ? 14 : 16} />
@@ -87,14 +85,7 @@ export function TimerControls({
<Icon icon="play" size={isCompact ? 14 : 16} /> <Icon icon="play" size={isCompact ? 14 : 16} />
)} )}
</IconButton> </IconButton>
<IconButton <IconButton size={"sm"} aria-label="End timer" disabled={disabled || !hasTimer} onClick={handleEnd}>
size={"sm"}
variant="destructive"
aria-label="End timer"
disabled={disabled || !hasTimer}
onClick={handleEnd}
className={"hover:opacity-70"}
>
<Icon icon="stop" size={isCompact ? 14 : 16} color={"var(--destructive)"} /> <Icon icon="stop" size={isCompact ? 14 : 16} color={"var(--destructive)"} />
</IconButton> </IconButton>
</div> </div>

View File

@@ -11,7 +11,7 @@ const buttonVariants = cva(
variant: { variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90", default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", "bg-destructive text-white hover:bg-destructive/80 active:bg-destructive/70 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 dark:hover:bg-destructive/70",
outline: "bg-transparent border dark:hover:bg-muted/40", outline: "bg-transparent border dark:hover:bg-muted/40",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",

View File

@@ -59,14 +59,14 @@ function DialogContent({
<DialogPrimitive.Close <DialogPrimitive.Close
data-slot="dialog-close" data-slot="dialog-close"
className={cn( className={cn(
"cursor-pointer ring-offset-background focus:ring-ring", "cursor-pointer",
"data-[state=open]:bg-accent data-[state=open]:text-muted-foreground", "data-[state=open]:bg-accent data-[state=open]:text-muted-foreground",
"absolute opacity-70", "absolute opacity-70",
closePos === "top-left" && "top-4 left-4", closePos === "top-left" && "top-4 left-4",
closePos === "top-right" && "top-4 right-4", closePos === "top-right" && "top-4 right-4",
closePos === "bottom-left" && "bottom-4 left-4", closePos === "bottom-left" && "bottom-4 left-4",
closePos === "bottom-right" && "bottom-4 right-4", closePos === "bottom-right" && "bottom-4 right-4",
"hover:opacity-100 focus:ring-2 focus:ring-offset-2 ", "hover:opacity-100",
"ocus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none", "ocus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none",
"[&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "[&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
)} )}

View File

@@ -7,8 +7,8 @@ const iconButtonVariants = cva(
{ {
variants: { variants: {
variant: { variant: {
default: "hover:text-foreground/70", default: "hover:text-foreground/70 hover:opacity-70",
destructive: "text-destructive hover:text-destructive/70", destructive: "text-destructive hover:opacity-70",
yellow: "text-yellow-500 hover:text-yellow-500/70", yellow: "text-yellow-500 hover:text-yellow-500/70",
green: "text-green-500 hover:text-green-500/70", green: "text-green-500 hover:text-green-500/70",
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",

View File

@@ -26,7 +26,7 @@ function Input({
className={cn( className={cn(
"border-input dark:bg-input/30 flex h-9 w-full min-w-0 items-center border bg-transparent", "border-input dark:bg-input/30 flex h-9 w-full min-w-0 items-center border bg-transparent",
"transition-[color,box-shadow]", "transition-[color,box-shadow]",
"has-[:focus-visible]:border-ring", "has-[:focus-visible]:border-[var(--personality)] ",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
"aria-invalid:border-destructive", "aria-invalid:border-destructive",
className, className,

View File

@@ -15,7 +15,7 @@ function Switch({
data-slot="switch" data-slot="switch"
data-size={size} data-size={size}
className={cn( className={cn(
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input", "peer data-[state=checked]:bg-personality data-[state=unchecked]:bg-input",
"focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80", "focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80",
"group/switch inline-flex shrink-0 items-center rounded-full border border-transparent", "group/switch inline-flex shrink-0 items-center rounded-full border border-transparent",
"outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50", "outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
@@ -27,7 +27,7 @@ function Switch({
<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 rounded-full ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0", "bg-background dark:data-[state=unchecked]:bg-personality dark:data-[state=checked]:bg-primary-foreground pointer-events-none block rounded-full ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0",
)} )}
/> />
</SwitchPrimitive.Root> </SwitchPrimitive.Root>

View File

@@ -13,6 +13,7 @@ export function UploadAvatar({
username, username,
avatarURL, avatarURL,
onAvatarUploaded, onAvatarUploaded,
skipOrgCheck = false,
className, className,
}: { }: {
name?: string; name?: string;
@@ -20,6 +21,7 @@ export function UploadAvatar({
avatarURL?: string | null; avatarURL?: string | null;
onAvatarUploaded: (avatarURL: string) => void; onAvatarUploaded: (avatarURL: string) => void;
label?: string; label?: string;
skipOrgCheck?: boolean;
className?: string; className?: string;
}) { }) {
const [uploading, setUploading] = useState(false); const [uploading, setUploading] = useState(false);
@@ -71,6 +73,7 @@ export function UploadAvatar({
size={24} size={24}
textClass={"text-4xl"} textClass={"text-4xl"}
strong strong
skipOrgCheck={skipOrgCheck}
/> />
{!uploading && showEdit && ( {!uploading && showEdit && (

View File

@@ -330,13 +330,13 @@ export default function Timeline() {
> >
<div <div
className={cn( className={cn(
"absolute inset-y-0 w-px bg-primary", "absolute inset-y-0 w-px bg-[var(--personality)]",
showTodayLabel && "mt-1", showTodayLabel && "mt-1",
)} )}
/> />
{showTodayLabel && ( {showTodayLabel && (
<div className="absolute -top-1.5"> <div className="absolute -top-1.5">
<span className="bg-primary px-1 py-0.5 text-[10px] font-semibold text-primary-foreground whitespace-nowrap"> <span className="bg-[var(--personality)] px-1 py-0.5 text-[10px] font-semibold text-[var(--background)] whitespace-nowrap">
TODAY TODAY
</span> </span>
</div> </div>

View File

@@ -1,7 +1,6 @@
# HIGH PRIORITY # HIGH PRIORITY
- BUGS: - BUGS:
- issue descriptions not showing
- FEATURES: - FEATURES:
- pricing page - pricing page
- see jira and other competitors - see jira and other competitors