diff --git a/README.md b/README.md index 1098753..eed715d 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,7 @@ src/ - **Generated GraphQL code is committed on purpose.** `src/graphql/generated/` (output of `npm run codegen`) is checked into git so CI can typecheck and build without holding the API token. Regenerate after changing any query/mutation; never edit by hand. - **Quality gates are CI-enforced, not hook-enforced.** There are deliberately no git hooks (husky/lint-staged): CI runs format check, lint, typecheck, tests, and build on every PR, and the same scripts run locally on demand. Hooks can be added later if commit-time enforcement proves necessary. -- **Card reaction icons are decorative.** The Figma shows attachment/fork/comment metrics on task cards, but the API exposes no such fields — the icons are kept for design fidelity without fabricating counts. +- **Card attachment/fork/comment icons are omitted.** The Figma shows those metrics on task cards, but the API's Task type exposes no fields for them. Per mentor guidance not to expose non-working UI, the icons are removed until the schema provides the data; the SVGs live in git history for easy reintroduction. - **Node 24 is a hard requirement** — `.npmrc` sets `engine-strict=true`, so `npm install` fails fast on older Node instead of warning. - **A11y deviation from the design, flagged and recommended per mentor guidance:** the Figma's active-tab red (`primary-4`, `#da584b`) on the dark surface measures ≈3.5:1 below WCAG AA's 4.5:1 for 15px text. Following the design team's process (flag + recommend), the active label uses `primary-3` (`#e27d73`) one step up the design system's own red scale measuring ≈4.7:1 (≈4.5:1 worst-case over the 5% gradient wash). The indicator bar stays `primary-4` (non-text graphic, 3:1 rule, passes). The active state is also conveyed non-visually via `aria-current="page"`. diff --git a/src/app/router.test.tsx b/src/app/router.test.tsx index cf2c7b3..7be1d1c 100644 --- a/src/app/router.test.tsx +++ b/src/app/router.test.tsx @@ -109,7 +109,7 @@ describe('dashboard main content', () => { renderAt('/') expect(screen.getByRole('img', { name: /grid view/i })).toBeInTheDocument() expect(screen.getByRole('img', { name: /list view/i })).toBeInTheDocument() - expect(screen.getAllByRole('img', { name: /add task/i }).length).toBeGreaterThan(0) + expect(screen.getAllByRole('button', { name: /add task/i }).length).toBeGreaterThan(0) }) it('marks the correct mobile tab active per route', () => { diff --git a/src/features/tasks/CreateTaskModal.tsx b/src/features/tasks/CreateTaskModal.tsx new file mode 100644 index 0000000..bcfb703 --- /dev/null +++ b/src/features/tasks/CreateTaskModal.tsx @@ -0,0 +1,399 @@ +import { useEffect, useRef, useState } from 'react' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import avatarUrl from '@/assets/avatar.png' +import { graphqlClient } from '@/lib/graphql-client' +import { CreateTaskDocument, UsersDocument } from '@/features/tasks/queries' +import { avatarSrc, POINTS, TAG_META } from '@/features/tasks/task-display' +import { DatePicker, DatePickerDialog } from '@/features/tasks/DatePicker' +import { + CalendarIcon, + CheckboxBlankIcon, + CheckboxCheckedIcon, + CloseIcon, + EstimateIcon, + LabelIcon, + UserIcon, +} from '@/features/tasks/icons' +import type { CreateTaskInput, PointEstimate, TaskTag } from '@/graphql/generated/graphql' + +const ESTIMATES: PointEstimate[] = ['ZERO', 'ONE', 'TWO', 'FOUR', 'EIGHT'] +const ALL_TAGS = Object.keys(TAG_META) as TaskTag[] + +type MenuName = 'estimate' | 'assignee' | 'label' | 'date' | null + +const formatDue = (iso: string) => { + const date = new Date(`${iso}T12:00:00`) + const month = date.toLocaleDateString('en-US', { month: 'short' }) + return `${month}. ${String(date.getDate())} ${String(date.getFullYear())}` +} + +export function CreateTaskModal({ onClose }: { onClose: () => void }) { + const [name, setName] = useState('') + const [estimate, setEstimate] = useState(null) + const [assigneeId, setAssigneeId] = useState(null) + const [tags, setTags] = useState([]) + const [dueDate, setDueDate] = useState('') + const [openMenu, setOpenMenu] = useState(null) + const [showValidation, setShowValidation] = useState(false) + const queryClient = useQueryClient() + + const users = useQuery({ + queryKey: ['users'], + queryFn: () => graphqlClient.request(UsersDocument), + }) + + const createTask = useMutation({ + mutationFn: (input: CreateTaskInput) => graphqlClient.request(CreateTaskDocument, { input }), + onSuccess: () => { + void queryClient.invalidateQueries({ queryKey: ['tasks'] }) + onClose() + }, + }) + + const containerRef = useRef(null) + const [opener] = useState(() => document.activeElement) + const isPending = createTask.isPending + + const dismiss = () => { + if (!isPending) onClose() + } + + useEffect(() => { + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape' && !isPending) onClose() + } + window.addEventListener('keydown', onKeyDown) + return () => { + window.removeEventListener('keydown', onKeyDown) + } + }, [onClose, isPending]) + + useEffect(() => { + return () => { + if (opener instanceof HTMLElement) opener.focus() + } + }, [opener]) + + const trapFocus = (event: React.KeyboardEvent) => { + if (event.key !== 'Tab' || containerRef.current === null) return + const focusables = [...containerRef.current.querySelectorAll('button, input')] + .filter((el) => el.tabIndex !== -1 && !el.hasAttribute('disabled')) + .filter((el) => el.getClientRects().length > 0) + const first = focusables.at(0) + const last = focusables.at(-1) + if (first === undefined || last === undefined) return + if (event.shiftKey && document.activeElement === first) { + event.preventDefault() + last.focus() + } else if (!event.shiftKey && document.activeElement === last) { + event.preventDefault() + first.focus() + } + } + + const selectedAssignee = users.data?.users.find((user) => user.id === assigneeId) + const canSubmit = name.trim().length > 0 && estimate !== null && dueDate !== '' + + const submit = () => { + if (createTask.isPending) return + if (!canSubmit) { + setShowValidation(true) + return + } + createTask.mutate({ + name: name.trim(), + pointEstimate: estimate, + dueDate: new Date(`${dueDate}T12:00:00Z`).toISOString(), + status: 'BACKLOG', + tags, + assigneeId, + }) + } + + const toggleMenu = (menu: MenuName) => { + setOpenMenu((current) => (current === menu ? null : menu)) + } + + const toggleTag = (tag: TaskTag) => { + setTags((current) => + current.includes(tag) ? current.filter((item) => item !== tag) : [...current, tag], + ) + } + + return ( +
+ + +
+ { + setName(event.target.value) + }} + className="w-full bg-transparent text-body-xl font-semibold text-neutral-1 placeholder:text-neutral-2" + /> +
+
+ + {openMenu === 'estimate' && ( +
+ + Estimate + + {ESTIMATES.map((option) => ( + + ))} +
+ )} +
+
+ + {openMenu === 'label' && ( +
+ + Tag Title + + {ALL_TAGS.map((tag) => ( + + ))} +
+ )} +
+
+ + {openMenu === 'assignee' && ( +
+ + Assign To... + + {(users.data?.users ?? []).map((user) => ( + + ))} +
+ )} +
+
+ + {openMenu === 'date' && ( + <> +
+ { + setDueDate(iso) + setOpenMenu(null) + }} + /> +
+
+ { + setOpenMenu(null) + }} + onConfirm={(iso) => { + setDueDate(iso) + setOpenMenu(null) + }} + /> +
+ + )} +
+
+ {(createTask.isError || (showValidation && !canSubmit)) && ( +
+ {canSubmit + ? 'The task could not be created. Check your connection and try again.' + : 'An estimate and a due date are required to create the task.'} +
+ )} +
+ + +
+ + + ) +} diff --git a/src/features/tasks/DatePicker.tsx b/src/features/tasks/DatePicker.tsx new file mode 100644 index 0000000..ad1bf67 --- /dev/null +++ b/src/features/tasks/DatePicker.tsx @@ -0,0 +1,292 @@ +import { useState } from 'react' +import { + ChevronLeftIcon, + ChevronRightIcon, + ChevronsLeftIcon, + ChevronsRightIcon, + ChevronThinRightIcon, +} from '@/features/tasks/icons' + +const WEEKDAYS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'] + +const toIso = (date: Date) => + `${String(date.getFullYear())}-${String(date.getMonth() + 1).padStart(2, '0')}-${String( + date.getDate(), + ).padStart(2, '0')}` + +const dayTone = (selected: boolean, isToday: boolean, inMonth: boolean) => { + if (selected) return 'bg-primary-4 text-neutral-1' + if (isToday) return 'border border-primary-4 text-neutral-1' + return inMonth ? 'text-neutral-1' : 'text-neutral-2' +} + +interface DatePickerProps { + value: string + onSelect: (iso: string) => void +} + +export function DatePicker({ value, onSelect }: DatePickerProps) { + const today = new Date() + const initial = value === '' ? today : new Date(`${value}T12:00:00`) + const [viewYear, setViewYear] = useState(initial.getFullYear()) + const [viewMonth, setViewMonth] = useState(initial.getMonth()) + + const moveView = (deltaMonths: number) => { + const next = new Date(viewYear, viewMonth + deltaMonths, 1) + setViewYear(next.getFullYear()) + setViewMonth(next.getMonth()) + } + + const monthLabel = new Date(viewYear, viewMonth, 1).toLocaleDateString('en-US', { + month: 'short', + year: 'numeric', + }) + const firstWeekday = new Date(viewYear, viewMonth, 1).getDay() + const cells = Array.from({ length: 42 }, (_, index) => { + return new Date(viewYear, viewMonth, index + 1 - firstWeekday) + }) + const todayIso = toIso(today) + + return ( +
+
+
+ + +
+ {monthLabel} +
+ + +
+
+
+
+
+ {WEEKDAYS.map((weekday) => ( +
+ {weekday} +
+ ))} +
+ {Array.from({ length: 6 }, (_, row) => ( +
+ {cells.slice(row * 7, row * 7 + 7).map((day) => { + const iso = toIso(day) + return ( +
+ +
+ ) + })} +
+ ))} +
+
+
+ +
+
+ ) +} + +const DIALOG_WEEKDAYS = ['S', 'M', 'T', 'W', 'T', 'F', 'S'] + +const dialogDayTone = (selected: boolean, isToday: boolean) => { + if (selected) return 'bg-primary-4' + if (isToday) return 'border-2 border-primary-3' + return '' +} + +interface DatePickerDialogProps { + value: string + onCancel: () => void + onConfirm: (iso: string) => void +} + +export function DatePickerDialog({ value, onCancel, onConfirm }: DatePickerDialogProps) { + const today = new Date() + const initial = value === '' ? today : new Date(`${value}T12:00:00`) + const [pending, setPending] = useState(toIso(initial)) + const [viewYear, setViewYear] = useState(initial.getFullYear()) + const [viewMonth, setViewMonth] = useState(initial.getMonth()) + + const moveView = (deltaMonths: number) => { + const next = new Date(viewYear, viewMonth + deltaMonths, 1) + setViewYear(next.getFullYear()) + setViewMonth(next.getMonth()) + } + + const pendingDate = new Date(`${pending}T12:00:00`) + const headline = pendingDate.toLocaleDateString('en-US', { + weekday: 'short', + month: 'short', + day: 'numeric', + }) + const monthLabel = new Date(viewYear, viewMonth, 1).toLocaleDateString('en-US', { + month: 'long', + year: 'numeric', + }) + const firstWeekday = new Date(viewYear, viewMonth, 1).getDay() + const daysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate() + const rowCount = Math.ceil((firstWeekday + daysInMonth) / 7) + const todayIso = toIso(today) + + return ( +
+
+ + {pendingDate.getFullYear()} + + {headline} +
+
+
+ + + {monthLabel} + + +
+
+ {DIALOG_WEEKDAYS.map((letter, index) => ( + + {letter} + + ))} +
+ {Array.from({ length: rowCount }, (_, row) => ( +
+ {Array.from({ length: 7 }, (_, column) => { + const dayNumber = row * 7 + column + 1 - firstWeekday + if (dayNumber < 1 || dayNumber > daysInMonth) { + return + } + const iso = toIso(new Date(viewYear, viewMonth, dayNumber)) + return ( + + ) + })} +
+ ))} +
+ + +
+
+
+ ) +} diff --git a/src/features/tasks/TaskCard.tsx b/src/features/tasks/TaskCard.tsx index b05b399..010a5c3 100644 --- a/src/features/tasks/TaskCard.tsx +++ b/src/features/tasks/TaskCard.tsx @@ -1,6 +1,12 @@ import avatarUrl from '@/assets/avatar.png' -import { AlarmIcon, AttachIcon, ChatIcon, DotsIcon, ForkIcon } from '@/features/tasks/icons' -import { dueInfo, pointsLabel, TAG_META, type TagTone } from '@/features/tasks/task-display' +import { AlarmIcon, DotsIcon } from '@/features/tasks/icons' +import { + avatarSrc, + dueInfo, + pointsLabel, + TAG_META, + type TagTone, +} from '@/features/tasks/task-display' import type { ApiTask } from '@/features/tasks/types' const tagToneClasses: Record = { @@ -44,17 +50,12 @@ export function TaskCard({ task }: { task: ApiTask }) { ))}
-
+
{task.assignee?.fullName -
- - - -
) diff --git a/src/features/tasks/Toolbar.tsx b/src/features/tasks/Toolbar.tsx index 22edf26..ba9bf06 100644 --- a/src/features/tasks/Toolbar.tsx +++ b/src/features/tasks/Toolbar.tsx @@ -1,9 +1,12 @@ +import { useState } from 'react' import { useLocation } from 'react-router' import { GridIcon, ListIcon } from '@/components/ui/icons' import { PlusIcon } from '@/features/tasks/icons' +import { CreateTaskModal } from '@/features/tasks/CreateTaskModal' export function Toolbar() { const { pathname } = useLocation() + const [createOpen, setCreateOpen] = useState(false) const onMyTask = pathname === '/my-task' return (
@@ -42,21 +45,36 @@ export function Toolbar() {
- { + setCreateOpen(true) + }} className="flex size-10 items-center justify-center rounded-lg bg-primary-4" > - +
- { + setCreateOpen(true) + }} className="fixed right-4 bottom-4 z-20 flex size-16 items-center justify-center rounded-full bg-primary-4 lg:hidden" > - + + {createOpen && ( + { + setCreateOpen(false) + }} + /> + )}
) } diff --git a/src/features/tasks/icons.tsx b/src/features/tasks/icons.tsx index 309be83..239dbf7 100644 --- a/src/features/tasks/icons.tsx +++ b/src/features/tasks/icons.tsx @@ -32,38 +32,149 @@ export function AlarmIcon(props: IconProps) { ) } -export function AttachIcon(props: IconProps) { +export function EstimateIcon(props: IconProps) { + return ( + + ) +} + +export function LabelIcon(props: IconProps) { + return ( + + ) +} + +export function CalendarIcon(props: IconProps) { + return ( + + ) +} + +export function UserIcon(props: IconProps) { + return ( + + ) +} + +export function CloseIcon(props: IconProps) { + return ( + + ) +} + +export function CheckboxCheckedIcon(props: IconProps) { + return ( + + ) +} + +export function CheckboxBlankIcon(props: IconProps) { + return ( + + ) +} + +export function ChevronLeftIcon(props: IconProps) { + return ( + + ) +} + +export function ChevronRightIcon(props: IconProps) { return ( ) } -export function ForkIcon(props: IconProps) { +export function ChevronsLeftIcon(props: IconProps) { return ( ) } -export function ChatIcon(props: IconProps) { +export function ChevronsRightIcon(props: IconProps) { return ( ) } + +export function ChevronThinRightIcon(props: IconProps) { + return ( + + ) +} diff --git a/src/features/tasks/queries.ts b/src/features/tasks/queries.ts index be02d45..39ca572 100644 --- a/src/features/tasks/queries.ts +++ b/src/features/tasks/queries.ts @@ -18,3 +18,32 @@ export const TasksDocument = graphql(` } } `) + +export const CreateTaskDocument = graphql(` + mutation CreateTask($input: CreateTaskInput!) { + createTask(input: $input) { + id + name + dueDate + pointEstimate + position + status + tags + assignee { + id + fullName + avatar + } + } + } +`) + +export const UsersDocument = graphql(` + query Users { + users { + id + fullName + avatar + } + } +`) diff --git a/src/features/tasks/task-display.ts b/src/features/tasks/task-display.ts index 11fcaa2..8bc50f0 100644 --- a/src/features/tasks/task-display.ts +++ b/src/features/tasks/task-display.ts @@ -20,7 +20,7 @@ export function groupTasksByStatus(tasks: ApiTask[]): BoardColumn[] { })) } -const POINTS: Record = { +export const POINTS: Record = { ZERO: 0, ONE: 1, TWO: 2, @@ -53,3 +53,8 @@ export function dueInfo(dueDate: string): { label: string; overdue: boolean } { overdue: dayDiff < 0, } } + +const LEGACY_DICEBEAR = /^https:\/\/avatars\.dicebear\.com\/api\/([^/]+)\/(.+)\.svg$/ + +export const avatarSrc = (avatar: string | null | undefined) => + avatar?.replace(LEGACY_DICEBEAR, 'https://api.dicebear.com/9.x/$1/svg?seed=$2') diff --git a/src/graphql/generated/gql.ts b/src/graphql/generated/gql.ts index b075a30..51281a0 100644 --- a/src/graphql/generated/gql.ts +++ b/src/graphql/generated/gql.ts @@ -15,9 +15,13 @@ import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document- */ type Documents = { "\n query Tasks($input: FilterTaskInput!) {\n tasks(input: $input) {\n id\n name\n dueDate\n pointEstimate\n position\n status\n tags\n assignee {\n id\n fullName\n avatar\n }\n }\n }\n": typeof types.TasksDocument, + "\n mutation CreateTask($input: CreateTaskInput!) {\n createTask(input: $input) {\n id\n name\n dueDate\n pointEstimate\n position\n status\n tags\n assignee {\n id\n fullName\n avatar\n }\n }\n }\n": typeof types.CreateTaskDocument, + "\n query Users {\n users {\n id\n fullName\n avatar\n }\n }\n": typeof types.UsersDocument, }; const documents: Documents = { "\n query Tasks($input: FilterTaskInput!) {\n tasks(input: $input) {\n id\n name\n dueDate\n pointEstimate\n position\n status\n tags\n assignee {\n id\n fullName\n avatar\n }\n }\n }\n": types.TasksDocument, + "\n mutation CreateTask($input: CreateTaskInput!) {\n createTask(input: $input) {\n id\n name\n dueDate\n pointEstimate\n position\n status\n tags\n assignee {\n id\n fullName\n avatar\n }\n }\n }\n": types.CreateTaskDocument, + "\n query Users {\n users {\n id\n fullName\n avatar\n }\n }\n": types.UsersDocument, }; /** @@ -38,6 +42,14 @@ export function graphql(source: string): unknown; * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function graphql(source: "\n query Tasks($input: FilterTaskInput!) {\n tasks(input: $input) {\n id\n name\n dueDate\n pointEstimate\n position\n status\n tags\n assignee {\n id\n fullName\n avatar\n }\n }\n }\n"): (typeof documents)["\n query Tasks($input: FilterTaskInput!) {\n tasks(input: $input) {\n id\n name\n dueDate\n pointEstimate\n position\n status\n tags\n assignee {\n id\n fullName\n avatar\n }\n }\n }\n"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "\n mutation CreateTask($input: CreateTaskInput!) {\n createTask(input: $input) {\n id\n name\n dueDate\n pointEstimate\n position\n status\n tags\n assignee {\n id\n fullName\n avatar\n }\n }\n }\n"): (typeof documents)["\n mutation CreateTask($input: CreateTaskInput!) {\n createTask(input: $input) {\n id\n name\n dueDate\n pointEstimate\n position\n status\n tags\n assignee {\n id\n fullName\n avatar\n }\n }\n }\n"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "\n query Users {\n users {\n id\n fullName\n avatar\n }\n }\n"): (typeof documents)["\n query Users {\n users {\n id\n fullName\n avatar\n }\n }\n"]; export function graphql(source: string) { return (documents as any)[source] ?? {}; diff --git a/src/graphql/generated/graphql.ts b/src/graphql/generated/graphql.ts index 16ddda5..88858c1 100644 --- a/src/graphql/generated/graphql.ts +++ b/src/graphql/generated/graphql.ts @@ -4,6 +4,15 @@ type Exact = { [K in keyof T]: T[K] }; /** Internal type. DO NOT USE DIRECTLY. */ export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; +export type CreateTaskInput = { + assigneeId?: string | null | undefined; + dueDate: string; + name: string; + pointEstimate: PointEstimate; + status: Status; + tags: Array; +}; + export type FilterTaskInput = { assigneeId?: string | null | undefined; dueDate?: string | null | undefined; @@ -45,5 +54,19 @@ export type TasksQueryVariables = Exact<{ export type TasksQuery = { tasks: Array<{ id: string, name: string, dueDate: string, pointEstimate: PointEstimate, position: number, status: Status, tags: Array, assignee: { id: string, fullName: string, avatar: string | null } | null }> }; +export type CreateTaskMutationVariables = Exact<{ + input: CreateTaskInput; +}>; + + +export type CreateTaskMutation = { createTask: { id: string, name: string, dueDate: string, pointEstimate: PointEstimate, position: number, status: Status, tags: Array, assignee: { id: string, fullName: string, avatar: string | null } | null } }; + +export type UsersQueryVariables = Exact<{ [key: string]: never; }>; + + +export type UsersQuery = { users: Array<{ id: string, fullName: string, avatar: string | null }> }; + -export const TasksDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Tasks"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"FilterTaskInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"tasks"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"dueDate"}},{"kind":"Field","name":{"kind":"Name","value":"pointEstimate"}},{"kind":"Field","name":{"kind":"Name","value":"position"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"tags"}},{"kind":"Field","name":{"kind":"Name","value":"assignee"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"fullName"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}}]}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file +export const TasksDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Tasks"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"FilterTaskInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"tasks"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"dueDate"}},{"kind":"Field","name":{"kind":"Name","value":"pointEstimate"}},{"kind":"Field","name":{"kind":"Name","value":"position"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"tags"}},{"kind":"Field","name":{"kind":"Name","value":"assignee"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"fullName"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}}]}}]}}]}}]} as unknown as DocumentNode; +export const CreateTaskDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateTask"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateTaskInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createTask"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"dueDate"}},{"kind":"Field","name":{"kind":"Name","value":"pointEstimate"}},{"kind":"Field","name":{"kind":"Name","value":"position"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"tags"}},{"kind":"Field","name":{"kind":"Name","value":"assignee"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"fullName"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}}]}}]}}]}}]} as unknown as DocumentNode; +export const UsersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Users"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"users"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"fullName"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/src/index.css b/src/index.css index ab7d682..edacf80 100644 --- a/src/index.css +++ b/src/index.css @@ -252,6 +252,14 @@ --text-body-s--line-height: 1.25rem; /* 20px */ --text-body-s--letter-spacing: 0.25px; + --text-picker: 0.875rem; /* 14px */ + --text-picker--line-height: 1.375rem; /* 22px */ + --text-picker--letter-spacing: 0px; + + --text-date-display: 1.75rem; /* 28px */ + --text-date-display--line-height: 2.5rem; /* 40px */ + --text-date-display--letter-spacing: 1px; + /* ============================================================ SHADOWS — [S] Depth scale + [P] Drop Shadow Large. Usage: `shadow-depth-2`, `shadow-drop-large`, `inset-shadow-depth`