From 75eace625a5a3d4ea893c416906d51a104b6b149 Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Thu, 18 Jun 2026 16:49:41 -0500 Subject: [PATCH 01/29] docs: design tailwind migration --- ...18-styled-components-to-tailwind-design.md | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-18-styled-components-to-tailwind-design.md diff --git a/docs/superpowers/specs/2026-06-18-styled-components-to-tailwind-design.md b/docs/superpowers/specs/2026-06-18-styled-components-to-tailwind-design.md new file mode 100644 index 000000000..321579a49 --- /dev/null +++ b/docs/superpowers/specs/2026-06-18-styled-components-to-tailwind-design.md @@ -0,0 +1,133 @@ +# Styled Components to Tailwind Migration Design + +## Objective + +Replace every Compass web use of `styled-components` with Tailwind CSS v4 while preserving the current visual design, interaction behavior, accessibility, and responsive layout. The finished repository must not depend on, configure, import, document, or test through `styled-components`. + +The migration must also leave theme-dependent styling behind semantic CSS custom properties so a future light theme can change token values without rewriting component classes. + +## Current State + +The web package currently combines two styling systems: + +- Tailwind v4 utilities and semantic tokens in `packages/web/src/index.css` +- `styled-components` across 60 source and test files, including shared primitives, forms, date pickers, menus, loaders, icons, Planner Sidebar surfaces, calendar grids, Week view, provider setup, and tests + +`packages/web/src/common/styles/theme.ts` is also used directly by non-styled TypeScript code for calculated colors, third-party component style objects, interaction overlays, and geometry constants. Removing the React theme provider does not require removing this plain TypeScript token adapter in the same step. + +## Chosen Approach + +Use a hybrid Tailwind architecture: + +1. Put one-off layout and state styling directly in JSX as Tailwind utilities. +2. Define stable reusable component recipes as `c-*` utilities in `packages/web/src/index.css`. +3. Use nested selectors inside `c-*` utilities for third-party DOM that Compass does not render directly, such as `react-datepicker` internals. +4. Pass runtime-only values through typed inline CSS custom properties, then consume them from static Tailwind classes or `c-*` utilities. +5. Keep all theme-dependent values behind semantic CSS custom properties. + +This avoids replacing `styled-components` with a large global component stylesheet while also avoiding unreadable repeated class strings for complex widgets. + +## Class Naming Rules + +Use `c-*` only for named, reusable Compass component recipes or complex selector scopes. Examples include `c-date-picker`, `c-context-menu-item`, and the existing `c-focus-ring`. + +Use regular Tailwind utilities for local layout, spacing, typography, and state. Do not prefix every Tailwind utility with `c-`; the prefix identifies Compass-owned abstractions rather than Tailwind itself. + +When a recipe has variants, prefer data attributes or CSS custom properties over generating class names dynamically. All class names must remain statically discoverable by Tailwind unless explicitly safelisted. + +## Theme Architecture + +`packages/web/src/index.css` remains the CSS source of truth for visual tokens. Components use semantic utilities such as `bg-bg-primary`, `text-text-light`, and `border-grid-line-primary`, never raw palette colors when a semantic token exists. + +The default dark token values remain unchanged. Token declarations will be structured so a future selector such as `[data-theme="light"]` can override the same custom properties. Adding a light theme or theme switcher is outside this migration. + +Runtime event colors, measured positions, visible-date counts, and similar values cannot be represented by a finite semantic palette. Components will expose those values as custom properties such as `--event-color` or `--visible-date-count`; static classes will reference the custom properties. + +`theme.ts` may remain as a plain typed object while non-CSS algorithms and third-party style APIs require JavaScript values. It must no longer import or implement `DefaultTheme`, and React must no longer require `ThemeProvider`. A later project may consolidate remaining JavaScript consumers onto exported token constants, but that is not required to remove `styled-components`. + +## Component Migration Strategy + +Migrate in dependency order: + +1. Shared primitives and icons: `Flex`, `Text`, `Input`, `Textarea`, `Button`, `IconButton`, `Divider`, `Focusable`, spinners, and icon wrappers. +2. Shared composite UI: context menus, loaders, date picker, and Not Found. +3. Forms: event form, recurrence controls, time/date controls, actions menu, and Someday form. +4. Planner Sidebar surfaces, including month picker and Someday event rows. +5. Shared calendar grid and Week view surfaces, including all-day rows, timed grids, headers, reminders, and edge indicators. +6. Provider and test cleanup after no rendered component depends on the styled theme context. + +Keep existing public component props when they express behavior. Styling-only props should become local class decisions, data attributes, or CSS custom properties. Avoid new barrel files. React components remain in their own files where a wrapper has behavior or a reusable API; trivial styled wrappers should collapse into their call site. + +## Fidelity Requirements + +The migration is not a redesign. Preserve: + +- element semantics and accessible names +- keyboard focus behavior and focus appearance +- hover, active, disabled, pending, selected, and drag states +- widths, heights, spacing, typography, borders, shadows, gradients, and stacking +- animation timing and reduced-motion behavior +- responsive and container-query behavior +- calendar event positioning, clipping, scrolling, and interaction hit areas +- third-party widget appearance and portal behavior + +Equivalent generated CSS is acceptable; DOM changes are acceptable only when they preserve semantics, event propagation, focus management, measurement, and layout. + +## Testing Strategy + +Use existing behavior tests as characterization coverage and add focused regression tests before changing styling behavior that is not currently protected. Tests should assert user-observable behavior, semantic state, or computed style through the existing Tailwind test stylesheet rather than coupling broadly to full class strings. + +For each migration slice: + +1. Run the smallest relevant web tests before changing the slice. +2. Add a failing characterization or regression test where a dynamic visual state lacks coverage. +3. Migrate the slice. +4. Run its focused tests and type checking. + +At the end, add or retain a repository guard that detects `styled-components` imports, manifest dependencies, Babel configuration, and obsolete documentation references. Then run: + +- `bun run test` +- `bun run test:e2e` +- `bun type-check` +- `bun lint` +- React Doctor on the changed React files +- `bun build:web` +- `bun build:backend` + +The full test commands may require local port access for MongoDB Memory Server and Playwright. Existing unrelated local worktrees must not be deleted; Jest discovery must instead be run in a way that excludes them or with those worktrees moved outside the repository by their owner. + +## Dependency and Configuration Removal + +After all runtime and test imports are gone: + +- remove `ThemeProvider` from `CompassProvider` and test wrappers +- remove `packages/web/src/common/styles/default-theme.d.ts` +- remove the styled-components Babel plugin configuration +- uninstall `styled-components` and `@types/styled-components` +- remove `babel-plugin-styled-components` if it is no longer a transitive or direct requirement +- regenerate `bun.lock` through Bun rather than editing lockfile entries manually +- verify a repository-wide search has no project-owned styled-components references + +## Documentation Changes + +Update project-owned documentation and agent guidance, including `CONTEXT.md`, `docs/Frontend/frontend-runtime-flow.md`, and `.cursorrules`, to describe Tailwind v4 as the web styling system. Document: + +- semantic theme tokens in `packages/web/src/index.css` +- when to create a `c-*` utility +- when to use local Tailwind utilities +- how runtime CSS custom properties are used +- the prohibition on raw colors when semantic tokens exist + +Vendored skill reference material that discusses CSS-in-JS generically is not Compass product documentation and is outside this migration. + +## Completion Criteria + +The migration is complete only when all of the following are proven from the current worktree: + +- no Compass runtime or test code imports `styled-components` +- no Compass manifest or build configuration depends on it +- project-owned documentation no longer describes or recommends it +- migrated UI behavior and visual states remain covered and pass +- full unit/package tests and Playwright e2e tests pass locally +- type checking, lint, React Doctor, web build, and backend build pass +- semantic CSS variables cover theme-dependent values, allowing future theme overrides without component rewrites From 86de2cf7b5494671a81c42365e90025a6548614b Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Thu, 18 Jun 2026 17:03:17 -0500 Subject: [PATCH 02/29] test(web): characterize theme tokens --- packages/web/src/common/styles/css.types.ts | 5 + .../web/src/common/styles/theme-css.test.ts | 54 +++++++++ packages/web/src/index.css | 114 ++++++++++++------ 3 files changed, 139 insertions(+), 34 deletions(-) create mode 100644 packages/web/src/common/styles/css.types.ts create mode 100644 packages/web/src/common/styles/theme-css.test.ts diff --git a/packages/web/src/common/styles/css.types.ts b/packages/web/src/common/styles/css.types.ts new file mode 100644 index 000000000..e286db2dc --- /dev/null +++ b/packages/web/src/common/styles/css.types.ts @@ -0,0 +1,5 @@ +import { type CSSProperties } from "react"; + +export type CSSVariables = CSSProperties & { + [name: `--${string}`]: string | number | undefined; +}; diff --git a/packages/web/src/common/styles/theme-css.test.ts b/packages/web/src/common/styles/theme-css.test.ts new file mode 100644 index 000000000..d7e30992b --- /dev/null +++ b/packages/web/src/common/styles/theme-css.test.ts @@ -0,0 +1,54 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "bun:test"; + +const indexCss = readFileSync(join(import.meta.dir, "../../index.css"), "utf8"); + +const semanticColorTokens = [ + "accent-primary", + "bg-primary", + "bg-secondary", + "border-primary", + "border-primary-dark", + "border-secondary", + "event-selected", + "fg-primary", + "fg-primary-dark", + "gradient-accent-light-start", + "gradient-accent-light-end", + "grid-line-primary", + "menu-bg", + "panel-badge-bg", + "panel-bg", + "panel-scrollbar", + "panel-scrollbar-active", + "panel-shadow", + "panel-text", + "shadow-default", + "status-success", + "status-error", + "status-warning", + "status-info", + "tag-one", + "tag-two", + "tag-three", + "text-light", + "text-lighter", + "text-light-inactive", + "text-dark", + "text-dark-placeholder", +] as const; + +describe("Tailwind theme CSS", () => { + it("maps every semantic color utility to a runtime theme variable", () => { + expect(indexCss).toContain("@theme inline"); + + for (const token of semanticColorTokens) { + expect(indexCss).toMatch( + new RegExp( + `--color-${token}:\\s*var\\(\\s*--compass-color-${token}\\s*\\);`, + ), + ); + } + }); +}); diff --git a/packages/web/src/index.css b/packages/web/src/index.css index 8a4316c62..44a50943a 100644 --- a/packages/web/src/index.css +++ b/packages/web/src/index.css @@ -3,6 +3,86 @@ /* Dynamic z-index css to support up to 100 overlapping agenda events */ @source inline("{hover:,focus:,}z-{{0..100..1}}"); +/* + * Runtime theme values. Future themes override these variables without + * changing component classes or rebuilding Tailwind utilities. + */ +:root { + --compass-color-accent-primary: hsl(202 100 67); + --compass-color-bg-primary: hsl(222 28 7); + --compass-color-bg-secondary: hsl(218 24 9); + --compass-color-border-primary: hsl(219 18 34 / 20%); + --compass-color-border-primary-dark: hsl(0 0 0 / 50.2%); + --compass-color-border-secondary: hsl(47 7 73); + --compass-color-event-selected: hsl(206 18 72); + --compass-color-fg-primary: hsl(47 7 73); + --compass-color-fg-primary-dark: hsl(208 13 71 / 54.9%); + --compass-color-gradient-accent-light-start: hsl(202 100 67); + --compass-color-gradient-accent-light-end: hsl(196 45 78); + --compass-color-grid-line-primary: hsl(219 18 34 / 20%); + --compass-color-menu-bg: hsl(0 0 98); + --compass-color-panel-badge-bg: hsl(0 0 100 / 7%); + --compass-color-panel-bg: hsl(219 8 46 / 20%); + --compass-color-panel-scrollbar: hsl(219 8 46 / 20%); + --compass-color-panel-scrollbar-active: hsl(221 9 37); + --compass-color-panel-shadow: hsl(221 9 37); + --compass-color-panel-text: hsl(0 0 98); + --compass-color-shadow-default: hsla(0 0 0 / 25%); + --compass-color-status-success: hsl(105 61 62); + --compass-color-status-error: hsl(0 63 60); + --compass-color-status-warning: hsl(25 100 63); + --compass-color-status-info: hsl(202 100 67); + --compass-color-tag-one: hsl(202 100 67); + --compass-color-tag-two: hsl(105 61 62); + --compass-color-tag-three: hsl(270 100 83); + --compass-color-text-light: hsl(47 7 73); + --compass-color-text-lighter: hsl(0 0 100); + --compass-color-text-light-inactive: hsl(208 13 71 / 54.9%); + --compass-color-text-dark: hsl(222 28 7); + --compass-color-text-dark-placeholder: hsl(219 8 46 / 90.2%); +} + +@theme inline { + --color-accent-primary: var(--compass-color-accent-primary); + --color-bg-primary: var(--compass-color-bg-primary); + --color-bg-secondary: var(--compass-color-bg-secondary); + --color-border-primary: var(--compass-color-border-primary); + --color-border-primary-dark: var(--compass-color-border-primary-dark); + --color-border-secondary: var(--compass-color-border-secondary); + --color-event-selected: var(--compass-color-event-selected); + --color-fg-primary: var(--compass-color-fg-primary); + --color-fg-primary-dark: var(--compass-color-fg-primary-dark); + --color-gradient-accent-light-start: var( + --compass-color-gradient-accent-light-start + ); + --color-gradient-accent-light-end: var( + --compass-color-gradient-accent-light-end + ); + --color-grid-line-primary: var(--compass-color-grid-line-primary); + --color-menu-bg: var(--compass-color-menu-bg); + --color-panel-badge-bg: var(--compass-color-panel-badge-bg); + --color-panel-bg: var(--compass-color-panel-bg); + --color-panel-scrollbar: var(--compass-color-panel-scrollbar); + --color-panel-scrollbar-active: var( + --compass-color-panel-scrollbar-active + ); + --color-panel-shadow: var(--compass-color-panel-shadow); + --color-panel-text: var(--compass-color-panel-text); + --color-shadow-default: var(--compass-color-shadow-default); + --color-status-success: var(--compass-color-status-success); + --color-status-error: var(--compass-color-status-error); + --color-status-warning: var(--compass-color-status-warning); + --color-status-info: var(--compass-color-status-info); + --color-tag-one: var(--compass-color-tag-one); + --color-tag-two: var(--compass-color-tag-two); + --color-tag-three: var(--compass-color-tag-three); + --color-text-light: var(--compass-color-text-light); + --color-text-lighter: var(--compass-color-text-lighter); + --color-text-light-inactive: var(--compass-color-text-light-inactive); + --color-text-dark: var(--compass-color-text-dark); + --color-text-dark-placeholder: var(--compass-color-text-dark-placeholder); +} + @theme { /* Font families */ --font-sans: @@ -14,40 +94,6 @@ ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New"; - /* Semantic colors */ - --color-accent-primary: hsl(202 100 67); - --color-bg-primary: hsl(222 28 7); - --color-bg-secondary: hsl(218 24 9); - --color-border-primary: hsl(219 18 34 / 20%); - --color-border-primary-dark: hsl(0 0 0 / 50.2%); - --color-border-secondary: hsl(47 7 73); - --color-event-selected: hsl(206 18 72); - --color-fg-primary: hsl(47 7 73); - --color-fg-primary-dark: hsl(208 13 71 / 54.9%); - --color-gradient-accent-light-start: hsl(202 100 67); - --color-gradient-accent-light-end: hsl(196 45 78); - --color-grid-line-primary: hsl(219 18 34 / 20%); - --color-menu-bg: hsl(0 0 98); - --color-panel-badge-bg: hsl(0 0 100 / 7%); - --color-panel-bg: hsl(219 8 46 / 20%); - --color-panel-scrollbar: hsl(219 8 46 / 20%); - --color-panel-scrollbar-active: hsl(221 9 37); - --color-panel-shadow: hsl(221 9 37); - --color-panel-text: hsl(0 0 98); - --color-shadow-default: hsla(0 0 0 / 25%); - --color-status-success: hsl(105 61 62); - --color-status-error: hsl(0 63 60); - --color-status-warning: hsl(25 100 63); - --color-status-info: hsl(202 100 67); - --color-tag-one: hsl(202 100 67); - --color-tag-two: hsl(105 61 62); - --color-tag-three: hsl(270 100 83); - --color-text-light: hsl(47 7 73); - --color-text-lighter: hsl(0 0 100); - --color-text-light-inactive: hsl(208 13 71 / 54.9%); - --color-text-dark: hsl(222 28 7); - --color-text-dark-placeholder: hsl(219 8 46 / 90.2%); - /* Font sizes */ --font-size-xs: 0.563rem; --font-size-s: 0.688rem; From 6cccbd7a428e0e0675e969465ef216a887df45a2 Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Thu, 18 Jun 2026 17:11:36 -0500 Subject: [PATCH 03/29] refactor(web): migrate shared primitives to tailwind --- .../components/CalendarAllDayEventCard.tsx | 5 +- .../components/CalendarAllDayRow.tsx | 2 +- .../components/CalendarTimedEventCard.tsx | 6 +- .../components/CalendarTimedGrid.tsx | 4 +- .../src/common/styles/animations/rotate.ts | 28 ------ .../common/styles/primitives-tailwind.test.ts | 62 ++++++++++++ .../web/src/common/styles/theme-css.test.ts | 5 +- .../AbsoluteOverflowLoader.tsx | 6 +- .../AbsoluteOverflowLoader/styled.ts | 2 +- packages/web/src/components/Button/Button.tsx | 96 +++++++++++++++++++ packages/web/src/components/Button/styled.ts | 59 ------------ .../src/components/DatePicker/DatePicker.tsx | 9 +- .../web/src/components/DatePicker/styled.ts | 4 +- .../web/src/components/Divider/Divider.tsx | 30 +++++- packages/web/src/components/Divider/index.ts | 3 - packages/web/src/components/Divider/styled.ts | 10 -- packages/web/src/components/Divider/types.ts | 1 + packages/web/src/components/Flex/Flex.tsx | 85 ++++++++++++++++ packages/web/src/components/Flex/index.ts | 3 - packages/web/src/components/Flex/styled.ts | 43 --------- .../src/components/Focusable/Focusable.tsx | 6 +- .../src/components/IconButton/IconButton.tsx | 28 +++++- .../web/src/components/IconButton/styled.ts | 53 ---------- .../web/src/components/Icons/Calendar.tsx | 11 +-- .../web/src/components/Icons/CircleTwo.tsx | 14 +-- packages/web/src/components/Icons/Command.tsx | 11 +-- packages/web/src/components/Icons/Flask.tsx | 11 +-- packages/web/src/components/Icons/List.tsx | 21 ++-- packages/web/src/components/Icons/Refresh.tsx | 14 +-- packages/web/src/components/Icons/Repeat.tsx | 14 +-- packages/web/src/components/Icons/Sidebar.tsx | 11 +-- packages/web/src/components/Icons/Spinner.tsx | 18 ++-- packages/web/src/components/Icons/Todo.tsx | 11 +-- packages/web/src/components/Icons/X.tsx | 17 ++-- .../web/src/components/Icons/icon.utils.ts | 6 ++ packages/web/src/components/Icons/styled.ts | 10 -- packages/web/src/components/Input/Input.tsx | 24 ++++- packages/web/src/components/Input/styled.ts | 24 ----- .../LoginAbsoluteOverflowLoader.tsx | 6 +- .../LoginAbsoluteOverflowLoader/styled.ts | 2 +- .../SomedayEventRectangle.tsx | 6 +- packages/web/src/components/Text/Text.tsx | 83 ++++++++++++++++ packages/web/src/components/Text/index.ts | 3 - packages/web/src/components/Text/styled.ts | 58 ----------- .../web/src/components/Textarea/Textarea.tsx | 29 +++++- packages/web/src/components/Textarea/index.ts | 3 - .../web/src/components/Textarea/styled.ts | 15 --- .../Description/TooltipDescription.tsx | 2 +- .../src/components/Tooltip/TooltipWrapper.tsx | 5 +- packages/web/src/index.css | 46 ++++++++- .../DatePickers/DatePickers.tsx | 2 +- .../DateTimeSection/DatePickers/styled.ts | 2 +- .../DateTimeSection/DateTimeSection.tsx | 2 +- .../TimePicker/TimePickers.tsx | 2 +- .../DateTimeSection/TimePicker/styled.ts | 2 +- .../DateTimeSection/styled.ts | 2 +- .../RecurrenceSection/RecurrenceSection.tsx | 2 +- .../components/EndsOnDate.tsx | 6 +- .../components/RecurrenceIntervalSelect.tsx | 4 +- .../RecurrenceSection/components/WeekDays.tsx | 4 +- .../RecurrenceSection/styled.ts | 2 +- .../Forms/EventForm/DuplicateMenuButton.tsx | 2 +- .../EventForm/MoveToSidebarMenuButton.tsx | 2 +- .../Forms/EventForm/PrioritySection/styled.ts | 2 +- .../EventForm/SaveSection/SaveSection.tsx | 8 +- .../web/src/views/Forms/EventForm/styled.ts | 6 +- packages/web/src/views/NotFound/NotFound.tsx | 2 +- .../Week/components/Grid/AllDayRow/styled.ts | 2 +- .../Week/components/Header/DayLabels.tsx | 2 +- .../views/Week/components/Header/Header.tsx | 2 +- packages/web/src/views/Week/styled.tsx | 2 +- 71 files changed, 613 insertions(+), 472 deletions(-) delete mode 100644 packages/web/src/common/styles/animations/rotate.ts create mode 100644 packages/web/src/common/styles/primitives-tailwind.test.ts create mode 100644 packages/web/src/components/Button/Button.tsx delete mode 100644 packages/web/src/components/Button/styled.ts delete mode 100644 packages/web/src/components/Divider/index.ts delete mode 100644 packages/web/src/components/Divider/styled.ts create mode 100644 packages/web/src/components/Flex/Flex.tsx delete mode 100644 packages/web/src/components/Flex/index.ts delete mode 100644 packages/web/src/components/Flex/styled.ts delete mode 100644 packages/web/src/components/IconButton/styled.ts create mode 100644 packages/web/src/components/Icons/icon.utils.ts delete mode 100644 packages/web/src/components/Icons/styled.ts delete mode 100644 packages/web/src/components/Input/styled.ts create mode 100644 packages/web/src/components/Text/Text.tsx delete mode 100644 packages/web/src/components/Text/index.ts delete mode 100644 packages/web/src/components/Text/styled.ts delete mode 100644 packages/web/src/components/Textarea/index.ts delete mode 100644 packages/web/src/components/Textarea/styled.ts diff --git a/packages/web/src/common/calendar-grid/components/CalendarAllDayEventCard.tsx b/packages/web/src/common/calendar-grid/components/CalendarAllDayEventCard.tsx index 42f264b0c..ac018f47e 100644 --- a/packages/web/src/common/calendar-grid/components/CalendarAllDayEventCard.tsx +++ b/packages/web/src/common/calendar-grid/components/CalendarAllDayEventCard.tsx @@ -20,10 +20,9 @@ import { gridHoverColorByPriority, } from "@web/common/styles/theme.util"; import { type Schema_GridEvent } from "@web/common/types/web.event.types"; -import { Flex } from "@web/components/Flex"; -import { AlignItems, FlexDirections } from "@web/components/Flex/styled"; +import { AlignItems, Flex, FlexDirections } from "@web/components/Flex/Flex"; import { SpaceCharacter } from "@web/components/SpaceCharacter"; -import { Text } from "@web/components/Text"; +import { Text } from "@web/components/Text/Text"; export interface CalendarAllDayEventCardProps { event: Schema_GridEvent; diff --git a/packages/web/src/common/calendar-grid/components/CalendarAllDayRow.tsx b/packages/web/src/common/calendar-grid/components/CalendarAllDayRow.tsx index c761ab369..5067c30d9 100644 --- a/packages/web/src/common/calendar-grid/components/CalendarAllDayRow.tsx +++ b/packages/web/src/common/calendar-grid/components/CalendarAllDayRow.tsx @@ -16,7 +16,7 @@ import { ID_ALLDAY_COLUMNS, ID_GRID_ALLDAY_ROW, } from "@web/common/constants/web.constants"; -import { Flex } from "@web/components/Flex"; +import { Flex } from "@web/components/Flex/Flex"; interface CalendarAllDayRowProps { allDayColumnsRef: RefCallback; diff --git a/packages/web/src/common/calendar-grid/components/CalendarTimedEventCard.tsx b/packages/web/src/common/calendar-grid/components/CalendarTimedEventCard.tsx index 6e2c0402e..7fe747a1d 100644 --- a/packages/web/src/common/calendar-grid/components/CalendarTimedEventCard.tsx +++ b/packages/web/src/common/calendar-grid/components/CalendarTimedEventCard.tsx @@ -33,13 +33,13 @@ import { import { type Schema_GridEvent } from "@web/common/types/web.event.types"; import { getTimesLabel } from "@web/common/utils/datetime/web.date.util"; import { getLineClamp } from "@web/common/utils/grid/grid.util"; -import { Flex } from "@web/components/Flex"; import { AlignItems, + Flex, FlexDirections, FlexWrap, -} from "@web/components/Flex/styled"; -import { Text } from "@web/components/Text"; +} from "@web/components/Flex/Flex"; +import { Text } from "@web/components/Text/Text"; export interface CalendarTimedEventCardProps { boxShadow?: CSSProperties["boxShadow"]; diff --git a/packages/web/src/common/calendar-grid/components/CalendarTimedGrid.tsx b/packages/web/src/common/calendar-grid/components/CalendarTimedGrid.tsx index 6027c6f1e..005639a9a 100644 --- a/packages/web/src/common/calendar-grid/components/CalendarTimedGrid.tsx +++ b/packages/web/src/common/calendar-grid/components/CalendarTimedGrid.tsx @@ -27,8 +27,8 @@ import { getHourLabels, } from "@web/common/utils/datetime/web.date.util"; import { getCurrentPercentOfDay } from "@web/common/utils/grid/grid.util"; -import { Flex } from "@web/components/Flex"; -import { Text } from "@web/components/Text"; +import { Flex } from "@web/components/Flex/Flex"; +import { Text } from "@web/components/Text/Text"; interface CalendarTimedGridProps { columnsId?: string; diff --git a/packages/web/src/common/styles/animations/rotate.ts b/packages/web/src/common/styles/animations/rotate.ts deleted file mode 100644 index 460c23508..000000000 --- a/packages/web/src/common/styles/animations/rotate.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { css, keyframes } from "styled-components"; - -export const rotate = keyframes` - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -`; - -export const rotateAnimation = < - T extends { - duration?: string; - loop?: number | "infinite" | "initial" | "inherit"; - paused?: boolean; - }, ->({ - duration, - loop, - paused, -}: T) => { - const pause = paused ? "paused" : "running"; - - return css` - ${rotate} ${duration ?? "1.1s"} linear ${loop ?? "infinite"} ${pause} - `; -}; diff --git a/packages/web/src/common/styles/primitives-tailwind.test.ts b/packages/web/src/common/styles/primitives-tailwind.test.ts new file mode 100644 index 000000000..c4e6f181c --- /dev/null +++ b/packages/web/src/common/styles/primitives-tailwind.test.ts @@ -0,0 +1,62 @@ +import { describe, expect, it } from "bun:test"; +import { existsSync, readFileSync } from "node:fs"; +import { join } from "node:path"; + +const webSrc = join(import.meta.dir, "../.."); + +const migratedFiles = [ + "components/Flex/Flex.tsx", + "components/Text/Text.tsx", + "components/Input/Input.tsx", + "components/Textarea/Textarea.tsx", + "components/IconButton/IconButton.tsx", + "components/Divider/Divider.tsx", + "components/Focusable/Focusable.tsx", + "components/Button/Button.tsx", + "components/Icons/Calendar.tsx", + "components/Icons/CircleTwo.tsx", + "components/Icons/Command.tsx", + "components/Icons/Flask.tsx", + "components/Icons/List.tsx", + "components/Icons/Refresh.tsx", + "components/Icons/Repeat.tsx", + "components/Icons/Sidebar.tsx", + "components/Icons/Spinner.tsx", + "components/Icons/Todo.tsx", + "components/Icons/X.tsx", +] as const; + +const removedFiles = [ + "components/Flex/index.ts", + "components/Flex/styled.ts", + "components/Text/index.ts", + "components/Text/styled.ts", + "components/Input/styled.ts", + "components/Textarea/styled.ts", + "components/Textarea/index.ts", + "components/IconButton/styled.ts", + "components/Divider/styled.ts", + "components/Divider/index.ts", + "components/Button/styled.ts", + "components/Icons/styled.ts", + "common/styles/animations/rotate.ts", +] as const; + +describe("Tailwind primitive architecture", () => { + it("keeps shared primitives independent from styled-components", () => { + for (const file of migratedFiles) { + const path = join(webSrc, file); + + expect(existsSync(path), `${file} should exist`).toBe(true); + expect(readFileSync(path, "utf8"), file).not.toContain( + "styled-components", + ); + } + + for (const file of removedFiles) { + expect(existsSync(join(webSrc, file)), `${file} should be removed`).toBe( + false, + ); + } + }); +}); diff --git a/packages/web/src/common/styles/theme-css.test.ts b/packages/web/src/common/styles/theme-css.test.ts index d7e30992b..d4d63a138 100644 --- a/packages/web/src/common/styles/theme-css.test.ts +++ b/packages/web/src/common/styles/theme-css.test.ts @@ -1,6 +1,6 @@ +import { describe, expect, it } from "bun:test"; import { readFileSync } from "node:fs"; import { join } from "node:path"; -import { describe, expect, it } from "bun:test"; const indexCss = readFileSync(join(import.meta.dir, "../../index.css"), "utf8"); @@ -37,6 +37,9 @@ const semanticColorTokens = [ "text-light-inactive", "text-dark", "text-dark-placeholder", + "text-divider", + "text-gradient-start", + "text-gradient-end", ] as const; describe("Tailwind theme CSS", () => { diff --git a/packages/web/src/components/AbsoluteOverflowLoader/AbsoluteOverflowLoader.tsx b/packages/web/src/components/AbsoluteOverflowLoader/AbsoluteOverflowLoader.tsx index 6bd604d0e..1b0169222 100644 --- a/packages/web/src/components/AbsoluteOverflowLoader/AbsoluteOverflowLoader.tsx +++ b/packages/web/src/components/AbsoluteOverflowLoader/AbsoluteOverflowLoader.tsx @@ -1,11 +1,11 @@ import { AlignItems, + type FlexProps, JustifyContent, - type Props, -} from "@web/components/Flex/styled"; +} from "@web/components/Flex/Flex"; import { Styled, StyledSpinner } from "./styled"; -export const AbsoluteOverflowLoader = (props: Props) => ( +export const AbsoluteOverflowLoader = (props: FlexProps) => ( > +>(({ className, ...props }, ref) => ( +
+)); + +Btn.displayName = "Btn"; + +export interface PriorityButtonProps + extends Omit, "color"> { + color?: string; + bordered?: boolean; + border?: string; +} + +export const PriorityButton = forwardRef< + HTMLDivElement, + PropsWithChildren +>(({ border, bordered, className, color, style, ...props }, ref) => { + const buttonStyle: CSSVariables = { + ...style, + "--priority-button-color": color, + "--priority-button-hover-color": color ? brighten(color) : undefined, + border: + border ?? + (bordered ? "2px solid var(--color-border-primary-dark)" : undefined), + }; + + return ( + + ); +}); + +PriorityButton.displayName = "PriorityButton"; + +interface SaveButtonProps extends PriorityButtonProps { + priority: Priority; + minWidth: number; + disabled?: boolean; +} + +export const SaveButton = forwardRef< + HTMLDivElement, + PropsWithChildren +>(({ className, disabled, minWidth, priority, style, ...props }, ref) => { + const background = darken(colorByPriority[priority]); + const hoverColor = + priority === Priorities.UNASSIGNED + ? "var(--color-text-light)" + : brighten(colorByPriority[priority]); + const buttonStyle: CSSVariables = { + ...style, + "--priority-button-hover-color": hoverColor, + background, + minWidth, + }; + + return ( + + ); +}); + +SaveButton.displayName = "SaveButton"; diff --git a/packages/web/src/components/Button/styled.ts b/packages/web/src/components/Button/styled.ts deleted file mode 100644 index 527d05ce3..000000000 --- a/packages/web/src/components/Button/styled.ts +++ /dev/null @@ -1,59 +0,0 @@ -import styled from "styled-components"; -import { Priorities, type Priority } from "@core/constants/core.constants"; -import { brighten, darken } from "@core/util/color.utils"; -import { colorByPriority } from "@web/common/styles/theme.util"; - -export const Btn = styled.div` - align-items: center; - border-radius: 2px; - display: flex; - justify-content: center; - cursor: pointer; -`; - -interface PalletteProps { - color?: string; - bordered?: boolean; - border?: string; -} - -export const PriorityButton = styled(Btn)` - background: color; - color: ${({ theme }) => theme.color.text.dark}; - min-width: 158px; - padding: 0 8px; - border: ${({ border, bordered, theme }) => - border || (bordered && `2px solid ${theme.color.border.primaryDark}`)}; - - &:hover { - background: ${({ theme }) => theme.color.bg.primary}; - color: ${({ color }) => brighten(color!)}; - transition: background-color 0.5s; - transition: color 0.55s; - } -`; -interface CustomProps { - priority: Priority; - minWidth: number; - disabled?: boolean; -} - -export const StyledSaveBtn = styled(PriorityButton)` - background: ${({ priority }) => darken(colorByPriority[priority])}; - color: ${({ theme }) => theme.color.text.dark} - - min-width: ${({ minWidth }) => minWidth}px; - - opacity: ${({ disabled }) => (disabled ? 0.5 : 1)}; - pointer-events: ${({ disabled }) => (disabled ? "none" : "auto")}; - - &:focus { - border: 2px solid ${({ theme }) => theme.color.border.primaryDark}; - } - &:hover { - color: ${({ priority, theme }) => - priority === Priorities.UNASSIGNED - ? theme.color.text.light - : brighten(colorByPriority[priority])}; - } -`; diff --git a/packages/web/src/components/DatePicker/DatePicker.tsx b/packages/web/src/components/DatePicker/DatePicker.tsx index 86b05b454..396640a71 100644 --- a/packages/web/src/components/DatePicker/DatePicker.tsx +++ b/packages/web/src/components/DatePicker/DatePicker.tsx @@ -14,10 +14,9 @@ import { StyledHeaderFlex, TodayStyledText, } from "@web/components/DatePicker/styled"; -import { Flex } from "@web/components/Flex"; -import { AlignItems, JustifyContent } from "@web/components/Flex/styled"; -import { StyledInput } from "@web/components/Input/styled"; -import { Text } from "@web/components/Text"; +import { AlignItems, Flex, JustifyContent } from "@web/components/Flex/Flex"; +import { InputBase } from "@web/components/Input/Input"; +import { Text } from "@web/components/Text/Text"; import { ChevronLeftIcon } from "@web/views/Day/components/Icons/ChevronLeftIcon"; import { ChevronRightIcon } from "@web/views/Day/components/Icons/ChevronRightIcon"; import { Focusable } from "../Focusable/Focusable"; @@ -84,7 +83,7 @@ export const DatePicker: React.FC = (datePickerProps) => { )} customInput={ > = ( - props, -) => { +export const Divider: React.FC> = ({ + className, + color, + style, + toggled: initialToggled, + width, + withAnimation: _withAnimation, + ...props +}) => { const [toggled, toggle] = useState(false); useEffect(() => { toggle(true); }, []); - return ; + const dividerStyle: CSSVariables = { + ...style, + background: getGradient(color ?? ""), + width: initialToggled === false || !toggled ? 0 : width || "100%", + }; + + return ( +
+ ); }; diff --git a/packages/web/src/components/Divider/index.ts b/packages/web/src/components/Divider/index.ts deleted file mode 100644 index f28f5851b..000000000 --- a/packages/web/src/components/Divider/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Divider } from "./Divider"; - -export { Divider }; diff --git a/packages/web/src/components/Divider/styled.ts b/packages/web/src/components/Divider/styled.ts deleted file mode 100644 index 0dac991e0..000000000 --- a/packages/web/src/components/Divider/styled.ts +++ /dev/null @@ -1,10 +0,0 @@ -import styled from "styled-components"; -import { getGradient } from "@web/common/styles/theme.util"; -import { type Props } from "./types"; - -export const StyledDivider = styled.div` - background: ${({ color }) => getGradient(color ?? "")}; - height: 2px; - width: ${({ toggled, width }) => (toggled ? width || "100%" : 0)}; - transition: ${({ theme }) => theme.transition.default}; -`; diff --git a/packages/web/src/components/Divider/types.ts b/packages/web/src/components/Divider/types.ts index e098d9a6c..1449a3301 100644 --- a/packages/web/src/components/Divider/types.ts +++ b/packages/web/src/components/Divider/types.ts @@ -1,4 +1,5 @@ export interface Props { + color?: string; toggled?: boolean; width?: string; withAnimation?: boolean; diff --git a/packages/web/src/components/Flex/Flex.tsx b/packages/web/src/components/Flex/Flex.tsx new file mode 100644 index 000000000..81d834e66 --- /dev/null +++ b/packages/web/src/components/Flex/Flex.tsx @@ -0,0 +1,85 @@ +import classNames from "classnames"; +import { forwardRef, type HTMLAttributes, type PropsWithChildren } from "react"; + +export enum FlexDirections { + COLUMN = "column", + COLUMN_REVERSE = "column-reverse", + ROW = "row", + ROW_REVERSE = "row-reverse", +} + +export enum JustifyContent { + CENTER = "center", + LEFT = "left", + SPACE_BETWEEN = "space-between", + SPACE_AROUND = "space-around", +} + +export enum AlignItems { + CENTER = "center", + BASELINE = "baseline", + FLEX_END = "flex-end", + FLEX_START = "flex-start", +} + +export enum FlexWrap { + WRAP = "wrap", + NO_WRAP = "no-wrap", + WRAP_REVERSE = "wrap-reverse", +} + +export interface FlexProps extends HTMLAttributes { + direction?: FlexDirections; + justifyContent?: JustifyContent; + alignItems?: AlignItems; + flexWrap?: FlexWrap; +} + +const directionClasses: Record = { + [FlexDirections.COLUMN]: "flex-col", + [FlexDirections.COLUMN_REVERSE]: "flex-col-reverse", + [FlexDirections.ROW]: "flex-row", + [FlexDirections.ROW_REVERSE]: "flex-row-reverse", +}; + +const justifyClasses: Record = { + [JustifyContent.CENTER]: "justify-center", + [JustifyContent.LEFT]: "justify-start", + [JustifyContent.SPACE_BETWEEN]: "justify-between", + [JustifyContent.SPACE_AROUND]: "justify-around", +}; + +const alignClasses: Record = { + [AlignItems.CENTER]: "items-center", + [AlignItems.BASELINE]: "items-baseline", + [AlignItems.FLEX_END]: "items-end", + [AlignItems.FLEX_START]: "items-start", +}; + +const wrapClasses: Record = { + [FlexWrap.WRAP]: "flex-wrap", + [FlexWrap.NO_WRAP]: "flex-nowrap", + [FlexWrap.WRAP_REVERSE]: "flex-wrap-reverse", +}; + +export const Flex = forwardRef>( + ( + { alignItems, className, direction, flexWrap, justifyContent, ...props }, + ref, + ) => ( +
+ ), +); + +Flex.displayName = "Flex"; diff --git a/packages/web/src/components/Flex/index.ts b/packages/web/src/components/Flex/index.ts deleted file mode 100644 index 775bbc095..000000000 --- a/packages/web/src/components/Flex/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Styled } from "./styled"; - -export const Flex = Styled; diff --git a/packages/web/src/components/Flex/styled.ts b/packages/web/src/components/Flex/styled.ts deleted file mode 100644 index d7775e4af..000000000 --- a/packages/web/src/components/Flex/styled.ts +++ /dev/null @@ -1,43 +0,0 @@ -import styled from "styled-components"; - -export enum FlexDirections { - COLUMN = "column", - COLUMN_REVERSE = "column-reverse", - ROW = "row", - ROW_REVERSE = "row-reverse", -} - -export enum JustifyContent { - CENTER = "center", - LEFT = "left", - SPACE_BETWEEN = "space-between", - SPACE_AROUND = "space-around", -} - -export enum AlignItems { - CENTER = "center", - BASELINE = "baseline", - FLEX_END = "flex-end", - FLEX_START = "flex-start", -} - -export enum FlexWrap { - WRAP = "wrap", - NO_WRAP = "no-wrap", - WRAP_REVERSE = "wrap-reverse", -} - -export interface Props { - direction?: FlexDirections; - justifyContent?: JustifyContent; - alignItems?: AlignItems; - flexWrap?: FlexWrap; -} - -export const Styled = styled.div` - align-items: ${({ alignItems }) => alignItems || "start"}; - display: flex; - flex-direction: ${({ direction }) => direction || "row"}; - flex-wrap: ${({ flexWrap }) => flexWrap || "nowrap"}; - justify-content: ${({ justifyContent }) => justifyContent || "start"}; -`; diff --git a/packages/web/src/components/Focusable/Focusable.tsx b/packages/web/src/components/Focusable/Focusable.tsx index 2873ea9a2..8d7964920 100644 --- a/packages/web/src/components/Focusable/Focusable.tsx +++ b/packages/web/src/components/Focusable/Focusable.tsx @@ -1,21 +1,21 @@ import { type ChangeEventHandler, + type ElementType, type FocusEvent, forwardRef, type HTMLAttributes, useCallback, useState, } from "react"; -import { type AnyStyledComponent } from "styled-components"; import { type UnderlinedInput } from "@web/common/types/component.types"; -import { Divider } from "@web/components/Divider"; +import { Divider } from "@web/components/Divider/Divider"; export interface Props extends UnderlinedInput, Omit, "onChange"> { autoFocus?: boolean; bgColor?: string; - Component: AnyStyledComponent; + Component: ElementType; name?: string; onChange?: ChangeEventHandler; placeholder?: string; diff --git a/packages/web/src/components/IconButton/IconButton.tsx b/packages/web/src/components/IconButton/IconButton.tsx index e180b9a4f..02e118c92 100644 --- a/packages/web/src/components/IconButton/IconButton.tsx +++ b/packages/web/src/components/IconButton/IconButton.tsx @@ -1,15 +1,37 @@ +import classNames from "classnames"; import type React from "react"; -import { type IconButtonProps, StyledIconButton } from "./styled"; + +export type IconButtonSize = "small" | "medium" | "large"; + +export interface IconButtonProps + extends React.ButtonHTMLAttributes { + size?: IconButtonSize; +} + +const sizeClasses: Record = { + small: "text-[20px]", + medium: "text-[27px]", + large: "text-[34px]", +}; const IconButton: React.FC = ({ size = "medium", children: icon, + className, ...props }) => { return ( - + ); }; diff --git a/packages/web/src/components/IconButton/styled.ts b/packages/web/src/components/IconButton/styled.ts deleted file mode 100644 index 95163b677..000000000 --- a/packages/web/src/components/IconButton/styled.ts +++ /dev/null @@ -1,53 +0,0 @@ -import styled from "styled-components"; - -export type IconButtonSize = "small" | "medium" | "large"; - -const sizeMap: Record = { - small: 20, - medium: 27, - large: 34, -}; - -export interface IconButtonProps - extends React.ButtonHTMLAttributes { - size?: IconButtonSize; -} - -const buttonStyleReset = ` - background: none; - color: inherit; - border: none; - padding: 0; - font: inherit; - cursor: pointer; - outline: inherit; -`; - -export const StyledIconButton = styled.button` - ${buttonStyleReset} - display: flex; - align-items: center; - justify-content: center; - border-radius: ${({ theme }) => theme.shape.borderRadius}; - transition: ${({ theme }) => theme.transition.default}; - font-size: ${({ size = "medium" }) => sizeMap[size]}px; - border: 2px solid transparent; - - &:hover { - transform: scale(1.05); - background-color: ${({ theme }) => theme.color.border.primary}; - } - - &:active { - transform: scale(0.95); - } - - &:disabled { - opacity: 0.6; - cursor: not-allowed; - } - - &:focus-visible { - box-shadow: 0 0 0 2px ${({ theme }) => theme.color.border.primaryDark}; - } -`; diff --git a/packages/web/src/components/Icons/Calendar.tsx b/packages/web/src/components/Icons/Calendar.tsx index e5efc9d49..21862318e 100644 --- a/packages/web/src/components/Icons/Calendar.tsx +++ b/packages/web/src/components/Icons/Calendar.tsx @@ -1,7 +1,6 @@ -import { CalendarDots } from "@phosphor-icons/react"; -import styled from "styled-components"; -import { iconStyles } from "./styled"; +import { CalendarDots, type IconProps } from "@phosphor-icons/react"; +import { getInteractiveIconClassName } from "./icon.utils"; -export const CalendarIcon = styled(CalendarDots)` - ${iconStyles} -`; +export const CalendarIcon = ({ className, ...props }: IconProps) => ( + +); diff --git a/packages/web/src/components/Icons/CircleTwo.tsx b/packages/web/src/components/Icons/CircleTwo.tsx index 3d8f20339..e65148828 100644 --- a/packages/web/src/components/Icons/CircleTwo.tsx +++ b/packages/web/src/components/Icons/CircleTwo.tsx @@ -1,7 +1,9 @@ -import { NumberCircleTwo } from "@phosphor-icons/react"; -import styled from "styled-components"; -import { iconStyles } from "./styled"; +import { type IconProps, NumberCircleTwo } from "@phosphor-icons/react"; +import { getInteractiveIconClassName } from "./icon.utils"; -export const CircleTwoIcon = styled(NumberCircleTwo)` - ${iconStyles} -`; +export const CircleTwoIcon = ({ className, ...props }: IconProps) => ( + +); diff --git a/packages/web/src/components/Icons/Command.tsx b/packages/web/src/components/Icons/Command.tsx index 791f31eca..44c55a071 100644 --- a/packages/web/src/components/Icons/Command.tsx +++ b/packages/web/src/components/Icons/Command.tsx @@ -1,7 +1,6 @@ -import { Command } from "@phosphor-icons/react"; -import styled from "styled-components"; -import { iconStyles } from "./styled"; +import { Command, type IconProps } from "@phosphor-icons/react"; +import { getInteractiveIconClassName } from "./icon.utils"; -export const CommandIcon = styled(Command)` - ${iconStyles} -`; +export const CommandIcon = ({ className, ...props }: IconProps) => ( + +); diff --git a/packages/web/src/components/Icons/Flask.tsx b/packages/web/src/components/Icons/Flask.tsx index e52468b1f..b12c9c7a3 100644 --- a/packages/web/src/components/Icons/Flask.tsx +++ b/packages/web/src/components/Icons/Flask.tsx @@ -1,7 +1,6 @@ -import { Flask } from "@phosphor-icons/react"; -import styled from "styled-components"; -import { iconStyles } from "./styled"; +import { Flask, type IconProps } from "@phosphor-icons/react"; +import { getInteractiveIconClassName } from "./icon.utils"; -export const FlaskIcon = styled(Flask)` - ${iconStyles} -`; +export const FlaskIcon = ({ className, ...props }: IconProps) => ( + +); diff --git a/packages/web/src/components/Icons/List.tsx b/packages/web/src/components/Icons/List.tsx index 7c87357c4..730ae34a6 100644 --- a/packages/web/src/components/Icons/List.tsx +++ b/packages/web/src/components/Icons/List.tsx @@ -1,12 +1,11 @@ -import { List } from "@phosphor-icons/react"; -import styled from "styled-components"; +import { type IconProps, List } from "@phosphor-icons/react"; +import { getInteractiveIconClassName } from "./icon.utils"; -export const StyledListIcon = styled(List)` - color: ${({ theme }) => theme.color.text.light}; - transition: filter 0.2s ease; - - &:hover { - cursor: pointer; - filter: brightness(130%); - } -`; +export const ListIcon = ({ className, ...props }: IconProps) => ( + +); diff --git a/packages/web/src/components/Icons/Refresh.tsx b/packages/web/src/components/Icons/Refresh.tsx index ad673913b..15511dc4b 100644 --- a/packages/web/src/components/Icons/Refresh.tsx +++ b/packages/web/src/components/Icons/Refresh.tsx @@ -1,7 +1,9 @@ -import { ArrowsClockwiseIcon } from "@phosphor-icons/react"; -import styled from "styled-components"; -import { iconStyles } from "./styled"; +import { ArrowsClockwiseIcon, type IconProps } from "@phosphor-icons/react"; +import { getInteractiveIconClassName } from "./icon.utils"; -export const RefreshIcon = styled(ArrowsClockwiseIcon)` - ${iconStyles} -`; +export const RefreshIcon = ({ className, ...props }: IconProps) => ( + +); diff --git a/packages/web/src/components/Icons/Repeat.tsx b/packages/web/src/components/Icons/Repeat.tsx index ffe87fa39..5971d7444 100644 --- a/packages/web/src/components/Icons/Repeat.tsx +++ b/packages/web/src/components/Icons/Repeat.tsx @@ -1,10 +1,6 @@ -import { Repeat } from "@phosphor-icons/react"; -import styled from "styled-components"; +import { type IconProps, Repeat } from "@phosphor-icons/react"; +import { getInteractiveIconClassName } from "./icon.utils"; -export const RepeatIcon = styled(Repeat)` - transition: filter 0.2s ease; - - &:hover { - filter: brightness(130%); - } -`; +export const RepeatIcon = ({ className, ...props }: IconProps) => ( + +); diff --git a/packages/web/src/components/Icons/Sidebar.tsx b/packages/web/src/components/Icons/Sidebar.tsx index 0b34f27b5..276d70056 100644 --- a/packages/web/src/components/Icons/Sidebar.tsx +++ b/packages/web/src/components/Icons/Sidebar.tsx @@ -1,7 +1,6 @@ -import { Sidebar } from "@phosphor-icons/react"; -import styled from "styled-components"; -import { iconStyles } from "./styled"; +import { type IconProps, Sidebar } from "@phosphor-icons/react"; +import { getInteractiveIconClassName } from "./icon.utils"; -export const SidebarIcon = styled(Sidebar)` - ${iconStyles} -`; +export const SidebarIcon = ({ className, ...props }: IconProps) => ( + +); diff --git a/packages/web/src/components/Icons/Spinner.tsx b/packages/web/src/components/Icons/Spinner.tsx index d8f985de4..4d59c34f8 100644 --- a/packages/web/src/components/Icons/Spinner.tsx +++ b/packages/web/src/components/Icons/Spinner.tsx @@ -1,9 +1,11 @@ -import { SpinnerGapIcon } from "@phosphor-icons/react"; -import styled from "styled-components"; -import { rotateAnimation } from "@web/common/styles/animations/rotate"; -import { iconStyles } from "./styled"; +import { type IconProps, SpinnerGapIcon } from "@phosphor-icons/react"; +import { getInteractiveIconClassName } from "./icon.utils"; -export const SpinnerIcon = styled(SpinnerGapIcon)` - ${iconStyles} - animation: ${rotateAnimation({})}; -`; +export const SpinnerIcon = ({ className, ...props }: IconProps) => ( + +); diff --git a/packages/web/src/components/Icons/Todo.tsx b/packages/web/src/components/Icons/Todo.tsx index 3e1c93afd..dd85226ed 100644 --- a/packages/web/src/components/Icons/Todo.tsx +++ b/packages/web/src/components/Icons/Todo.tsx @@ -1,7 +1,6 @@ -import { CheckCircle } from "@phosphor-icons/react"; -import styled from "styled-components"; -import { iconStyles } from "./styled"; +import { CheckCircle, type IconProps } from "@phosphor-icons/react"; +import { getInteractiveIconClassName } from "./icon.utils"; -export const TodoIcon = styled(CheckCircle)` - ${iconStyles} -`; +export const TodoIcon = ({ className, ...props }: IconProps) => ( + +); diff --git a/packages/web/src/components/Icons/X.tsx b/packages/web/src/components/Icons/X.tsx index afbef8170..1aa0df8e7 100644 --- a/packages/web/src/components/Icons/X.tsx +++ b/packages/web/src/components/Icons/X.tsx @@ -1,10 +1,9 @@ -import { X } from "@phosphor-icons/react"; -import styled from "styled-components"; +import { type IconProps, X } from "@phosphor-icons/react"; +import { getInteractiveIconClassName } from "./icon.utils"; -export const StyledXIcon = styled(X)` - transition: filter 0.2s ease; - - &:hover { - filter: brightness(150%); - } -`; +export const XIcon = ({ className, ...props }: IconProps) => ( + +); diff --git a/packages/web/src/components/Icons/icon.utils.ts b/packages/web/src/components/Icons/icon.utils.ts new file mode 100644 index 000000000..eb27c0c45 --- /dev/null +++ b/packages/web/src/components/Icons/icon.utils.ts @@ -0,0 +1,6 @@ +import classNames from "classnames"; + +export const getInteractiveIconClassName = ( + className?: string, + hoverBrightnessClass?: string, +) => classNames("c-icon", hoverBrightnessClass, className); diff --git a/packages/web/src/components/Icons/styled.ts b/packages/web/src/components/Icons/styled.ts deleted file mode 100644 index 741f449b1..000000000 --- a/packages/web/src/components/Icons/styled.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { type IconProps } from "@phosphor-icons/react"; -import { css } from "styled-components"; - -export const iconStyles = css` - transition: filter 0.2s ease; - - &:hover { - filter: brightness(130%); - } -`; diff --git a/packages/web/src/components/Input/Input.tsx b/packages/web/src/components/Input/Input.tsx index 8d98de20d..d46613eab 100644 --- a/packages/web/src/components/Input/Input.tsx +++ b/packages/web/src/components/Input/Input.tsx @@ -1,3 +1,4 @@ +import classNames from "classnames"; import { type ForwardRefRenderFunction, forwardRef, @@ -9,23 +10,40 @@ import { type UnderlinedInput, } from "@web/common/types/component.types"; import { Focusable } from "../Focusable/Focusable"; -import { StyledInput, type Props as StyledProps } from "./styled"; export interface Props extends ClassNamedComponent, UnderlinedInput, - StyledProps, InputHTMLAttributes { autoFocus?: boolean; bgColor?: string; } +export const InputBase = forwardRef( + ( + { bgColor, className, style, withUnderline: _withUnderline, ...props }, + ref, + ) => ( + + ), +); + +InputBase.displayName = "InputBase"; + const InputComponent: ForwardRefRenderFunction = ( { withUnderline = true, ...props }: Props, ref: Ref, ) => ( ` - background-color: ${({ bgColor }) => bgColor}; - border: none; - height: 34px; - padding: 0 8px; - font-size: ${({ theme }) => theme.text.size.l}; - outline: none; - width: 100%; - transition: ${({ theme }) => theme.transition.default}; - - ::placeholder { - color: ${({ theme }) => theme.color.text.darkPlaceholder}; - } - - &:hover { - background-color: ${({ theme }) => theme.color.border.primary}; - } -`; diff --git a/packages/web/src/components/LoginAbsoluteOverflowLoader/LoginAbsoluteOverflowLoader.tsx b/packages/web/src/components/LoginAbsoluteOverflowLoader/LoginAbsoluteOverflowLoader.tsx index 6ae61f8fa..9d7e1da40 100644 --- a/packages/web/src/components/LoginAbsoluteOverflowLoader/LoginAbsoluteOverflowLoader.tsx +++ b/packages/web/src/components/LoginAbsoluteOverflowLoader/LoginAbsoluteOverflowLoader.tsx @@ -1,15 +1,15 @@ import { useEffect, useState } from "react"; import { AlignItems, + type FlexProps, JustifyContent, - type Props, -} from "@web/components/Flex/styled"; +} from "@web/components/Flex/Flex"; import { LoadingMessage, ProgressBar, Styled, StyledSpinner } from "./styled"; /** * Absolute overflow loader customized to handle login flow */ -export const LoginAbsoluteOverflowLoader = (props: Props) => { +export const LoginAbsoluteOverflowLoader = (props: FlexProps) => { const [showMessage, setShowMessage] = useState(false); const [currentMessage, setCurrentMessage] = useState(MESSAGE_TEXT); diff --git a/packages/web/src/components/LoginAbsoluteOverflowLoader/styled.ts b/packages/web/src/components/LoginAbsoluteOverflowLoader/styled.ts index fc73df29d..4ead29a41 100644 --- a/packages/web/src/components/LoginAbsoluteOverflowLoader/styled.ts +++ b/packages/web/src/components/LoginAbsoluteOverflowLoader/styled.ts @@ -1,6 +1,6 @@ import styled, { keyframes } from "styled-components"; import { c } from "@web/common/styles/colors"; -import { Flex } from "@web/components/Flex"; +import { Flex } from "@web/components/Flex/Flex"; export const Styled = styled(Flex)` position: absolute; diff --git a/packages/web/src/components/PlannerSidebar/SomedayEventSections/SomedayEvents/SomedayEventContainer/SomedayEventRectangle.tsx b/packages/web/src/components/PlannerSidebar/SomedayEventSections/SomedayEvents/SomedayEventContainer/SomedayEventRectangle.tsx index f0e905a45..41234298e 100644 --- a/packages/web/src/components/PlannerSidebar/SomedayEventSections/SomedayEvents/SomedayEventContainer/SomedayEventRectangle.tsx +++ b/packages/web/src/components/PlannerSidebar/SomedayEventSections/SomedayEvents/SomedayEventContainer/SomedayEventRectangle.tsx @@ -5,14 +5,14 @@ import { DotsSixVertical, } from "@phosphor-icons/react"; import { Categories_Event, type Schema_Event } from "@core/types/event.types"; -import { Flex } from "@web/components/Flex"; import { AlignItems, + Flex, FlexDirections, JustifyContent, -} from "@web/components/Flex/styled"; +} from "@web/components/Flex/Flex"; import { type Actions_Sidebar } from "@web/components/PlannerSidebar/draft/hooks/useSidebarActions"; -import { Text } from "@web/components/Text"; +import { Text } from "@web/components/Text/Text"; import { type Props_DraftForm } from "@web/views/Week/components/Draft/context/DraftContext"; const ACTIONS_CLASS_NAME = diff --git a/packages/web/src/components/Text/Text.tsx b/packages/web/src/components/Text/Text.tsx new file mode 100644 index 000000000..2603a1742 --- /dev/null +++ b/packages/web/src/components/Text/Text.tsx @@ -0,0 +1,83 @@ +import classNames from "classnames"; +import { forwardRef, type HTMLAttributes, type PropsWithChildren } from "react"; +import { type CSSVariables } from "@web/common/styles/css.types"; +import { getGradient } from "@web/common/styles/theme.util"; + +type FontSize = "xs" | "s" | "m" | "l" | "xl" | "xxl" | "xxxl" | "4xl" | "5xl"; + +export interface TextProps + extends Omit, "color"> { + bgColor?: string; + withBottomBorder?: boolean; + color?: string; + cursor?: string; + fontWeight?: number | "normal" | "bold" | "bolder" | "lighter"; + lineHeight?: number; + size?: FontSize; + withGradient?: boolean; + withUnderline?: boolean; + zIndex?: number; +} + +const sizeClasses: Record = { + xs: "text-xs", + s: "text-s", + m: "text-m", + l: "text-l", + xl: "text-xl", + xxl: "text-xxl", + xxxl: "text-xxxl", + "4xl": "text-4xl", + "5xl": "text-5xl", +}; + +export const Text = forwardRef>( + ( + { + bgColor: _bgColor, + className, + color, + cursor, + fontWeight = "normal", + lineHeight, + size, + style, + withBottomBorder, + withGradient, + withUnderline = false, + zIndex, + ...props + }, + ref, + ) => { + const textStyle: CSSVariables = { + ...style, + "--text-underline-gradient": getGradient(color ?? ""), + color, + cursor: (withUnderline + ? (cursor ?? "pointer") + : cursor) as CSSVariables["cursor"], + fontWeight, + lineHeight: lineHeight ? `${lineHeight}px` : undefined, + zIndex, + }; + + return ( + + ); + }, +); + +Text.displayName = "Text"; diff --git a/packages/web/src/components/Text/index.ts b/packages/web/src/components/Text/index.ts deleted file mode 100644 index 0799bedfa..000000000 --- a/packages/web/src/components/Text/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { StyledText } from "./styled"; - -export const Text = StyledText; diff --git a/packages/web/src/components/Text/styled.ts b/packages/web/src/components/Text/styled.ts deleted file mode 100644 index 639a864c5..000000000 --- a/packages/web/src/components/Text/styled.ts +++ /dev/null @@ -1,58 +0,0 @@ -import styled from "styled-components"; -import { theme } from "@web/common/styles/theme"; -import { blueGradient, getGradient } from "@web/common/styles/theme.util"; - -export interface Props { - bgColor?: string; - withBottomBorder?: boolean; - color?: string; - cursor?: string; - fontWeight?: number | "normal" | "bold" | "bolder" | "lighter"; - lineHeight?: number; - size?: FontSize; - withGradient?: boolean; - withUnderline?: boolean; - zIndex?: number; -} - -type FontSize = "xs" | "s" | "m" | "l" | "xl" | "xxl" | "xxxl" | "4xl" | "5xl"; - -export const StyledText = styled.span` - ${({ color }) => color && `color: ${color};`} - ${({ cursor }) => cursor && `cursor: ${cursor};`} - ${({ lineHeight }) => lineHeight && `line-height: ${lineHeight}px;`} - ${({ size }) => size && `font-size: ${theme.text.size[size]};`} - ${({ withBottomBorder }) => - withBottomBorder && `border-bottom: 1px solid #5f5f5f;`} - - - font-weight: ${({ fontWeight = "normal" }) => fontWeight}; - position: relative; - - ${({ withGradient }) => - withGradient && - ` - color: transparent; - background: ${blueGradient}; - background-clip: text; - -webkit-background-clip: text; - `} - - ${({ color, cursor, withUnderline = false }) => - withUnderline && - ` - cursor: ${cursor || "pointer"}; - &:hover { - &::after { - content: ' '; - background: ${getGradient(color ?? "")}; - position: absolute; - width: 100%; - height: 2px; - left: 0; - bottom: 0; - } - } - `} - ${({ zIndex }) => zIndex && `z-index: ${zIndex}`} -`; diff --git a/packages/web/src/components/Textarea/Textarea.tsx b/packages/web/src/components/Textarea/Textarea.tsx index c3727db13..c3e3da1be 100644 --- a/packages/web/src/components/Textarea/Textarea.tsx +++ b/packages/web/src/components/Textarea/Textarea.tsx @@ -1,13 +1,38 @@ +import classNames from "classnames"; import { type ForwardedRef, type ForwardRefRenderFunction, forwardRef, useRef, } from "react"; +import TextareaAutoSize from "react-textarea-autosize"; import { Focusable } from "@web/components/Focusable/Focusable"; -import { StyledTextarea } from "./styled"; import { type TextareaProps } from "./types"; +const TextareaBase = forwardRef( + ( + { + className, + heightFitsContent: _heightFitsContent, + underlineColor: _underlineColor, + withUnderline: _withUnderline, + ...props + }, + ref, + ) => ( + + ), +); + +TextareaBase.displayName = "TextareaBase"; + const _Textarea: ForwardRefRenderFunction< HTMLTextAreaElement, TextareaProps @@ -20,7 +45,7 @@ const _Textarea: ForwardRefRenderFunction< return ( ` - border: none; - outline: none; - font-weight: 600; - width: 100%; - resize: none; - - ::placeholder { - color: ${({ theme }) => theme.color.text.darkPlaceholder}; - } -`; diff --git a/packages/web/src/components/Tooltip/Description/TooltipDescription.tsx b/packages/web/src/components/Tooltip/Description/TooltipDescription.tsx index c306449dc..c17705e89 100644 --- a/packages/web/src/components/Tooltip/Description/TooltipDescription.tsx +++ b/packages/web/src/components/Tooltip/Description/TooltipDescription.tsx @@ -1,5 +1,5 @@ import { type FC } from "react"; -import { Text } from "@web/components/Text"; +import { Text } from "@web/components/Text/Text"; interface Props { description: string; diff --git a/packages/web/src/components/Tooltip/TooltipWrapper.tsx b/packages/web/src/components/Tooltip/TooltipWrapper.tsx index 3212a19e1..b5ae3c045 100644 --- a/packages/web/src/components/Tooltip/TooltipWrapper.tsx +++ b/packages/web/src/components/Tooltip/TooltipWrapper.tsx @@ -1,14 +1,13 @@ import type React from "react"; import { type ReactNode } from "react"; -import { AlignItems } from "@web/components/Flex/styled"; -import { Text } from "@web/components/Text"; +import { AlignItems, Flex } from "@web/components/Flex/Flex"; +import { Text } from "@web/components/Text/Text"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@web/components/Tooltip"; import { type TooltipOptions } from "@web/components/Tooltip/tooltip.types"; -import { Flex } from "../Flex"; import { LegacyShortcutHint } from "../Shortcuts/ShortcutHint"; import { TooltipDescription } from "./Description/TooltipDescription"; diff --git a/packages/web/src/index.css b/packages/web/src/index.css index 44a50943a..1c14c1f63 100644 --- a/packages/web/src/index.css +++ b/packages/web/src/index.css @@ -40,6 +40,9 @@ --compass-color-text-light-inactive: hsl(208 13 71 / 54.9%); --compass-color-text-dark: hsl(222 28 7); --compass-color-text-dark-placeholder: hsl(219 8 46 / 90.2%); + --compass-color-text-divider: #5f5f5f; + --compass-color-text-gradient-start: hsl(202 100 67); + --compass-color-text-gradient-end: hsl(195 78 56); } @theme inline { @@ -63,9 +66,7 @@ --color-panel-badge-bg: var(--compass-color-panel-badge-bg); --color-panel-bg: var(--compass-color-panel-bg); --color-panel-scrollbar: var(--compass-color-panel-scrollbar); - --color-panel-scrollbar-active: var( - --compass-color-panel-scrollbar-active - ); + --color-panel-scrollbar-active: var(--compass-color-panel-scrollbar-active); --color-panel-shadow: var(--compass-color-panel-shadow); --color-panel-text: var(--compass-color-panel-text); --color-shadow-default: var(--compass-color-shadow-default); @@ -81,6 +82,9 @@ --color-text-light-inactive: var(--compass-color-text-light-inactive); --color-text-dark: var(--compass-color-text-dark); --color-text-dark-placeholder: var(--compass-color-text-dark-placeholder); + --color-text-divider: var(--compass-color-text-divider); + --color-text-gradient-start: var(--compass-color-text-gradient-start); + --color-text-gradient-end: var(--compass-color-text-gradient-end); } @theme { @@ -135,6 +139,7 @@ cubic-bezier(0.16, 1, 0.3, 1) both; --animate-someday-cold-fade-in: somedayColdFadeIn 600ms cubic-bezier(0.16, 1, 0.3, 1) both; + --animate-spinner-rotate: spinnerRotate 1.1s linear infinite; @keyframes progressSlide { 0% { @@ -197,6 +202,15 @@ opacity: 1; } } + + @keyframes spinnerRotate { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } + } } @media (prefers-reduced-motion: reduce) { @@ -285,6 +299,32 @@ } } +@utility c-text-gradient { + color: transparent; + background: linear-gradient( + var(--compass-color-text-gradient-start), + var(--compass-color-text-gradient-end) + ); + background-clip: text; + -webkit-background-clip: text; +} + +@utility c-icon { + @apply transition-[filter] duration-200 hover:brightness-[1.3]; +} + +@utility c-text-underline { + &:hover::after { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 2px; + background: var(--text-underline-gradient); + content: " "; + } +} + @utility compass-scroll { @layer components { :focus-visible { diff --git a/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/DatePickers/DatePickers.tsx b/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/DatePickers/DatePickers.tsx index ba3963e05..a364cbe24 100644 --- a/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/DatePickers/DatePickers.tsx +++ b/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/DatePickers/DatePickers.tsx @@ -6,7 +6,7 @@ import dayjs from "@core/util/date/dayjs"; import { dateIsValid } from "@web/common/utils/datetime/web.date.util"; import { shouldAdjustComplimentDate } from "@web/common/utils/datetime/web.datetime.util"; import { DatePicker } from "@web/components/DatePicker/DatePicker"; -import { AlignItems } from "@web/components/Flex/styled"; +import { AlignItems } from "@web/components/Flex/Flex"; import { StyledDateFlex } from "@web/views/Forms/EventForm/DateControlsSection/DateTimeSection/DatePickers/styled"; import { type SetEventFormField } from "@web/views/Forms/EventForm/types"; diff --git a/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/DatePickers/styled.ts b/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/DatePickers/styled.ts index 691d94d50..98e115da5 100644 --- a/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/DatePickers/styled.ts +++ b/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/DatePickers/styled.ts @@ -1,5 +1,5 @@ import styled from "styled-components"; -import { Flex } from "@web/components/Flex"; +import { Flex } from "@web/components/Flex/Flex"; export const StyledDateFlex = styled(Flex)` width: 120px; diff --git a/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/DateTimeSection.tsx b/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/DateTimeSection.tsx index 856210026..ef897e417 100644 --- a/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/DateTimeSection.tsx +++ b/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/DateTimeSection.tsx @@ -1,7 +1,7 @@ import { type FC, type SetStateAction } from "react"; import { Categories_Event, type Schema_Event } from "@core/types/event.types"; import { type SelectOption } from "@web/common/types/component.types"; -import { AlignItems } from "@web/components/Flex/styled"; +import { AlignItems } from "@web/components/Flex/Flex"; import { DatePickers } from "@web/views/Forms/EventForm/DateControlsSection/DateTimeSection/DatePickers/DatePickers"; import { StyledDateTimeFlex } from "@web/views/Forms/EventForm/DateControlsSection/DateTimeSection/styled"; import { TimePickers } from "@web/views/Forms/EventForm/DateControlsSection/DateTimeSection/TimePicker/TimePickers"; diff --git a/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/TimePicker/TimePickers.tsx b/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/TimePicker/TimePickers.tsx index 860daa5e4..17d167c62 100644 --- a/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/TimePicker/TimePickers.tsx +++ b/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/TimePicker/TimePickers.tsx @@ -8,7 +8,7 @@ import { mapToBackend, } from "@web/common/utils/datetime/web.date.util"; import { shouldAdjustComplimentTime } from "@web/common/utils/datetime/web.datetime.util"; -import { AlignItems } from "@web/components/Flex/styled"; +import { AlignItems } from "@web/components/Flex/Flex"; import { StyledTimeFlex } from "../styled"; import { TimePicker } from "./TimePicker"; diff --git a/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/TimePicker/styled.ts b/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/TimePicker/styled.ts index 0dc4dd99c..681a49d1c 100644 --- a/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/TimePicker/styled.ts +++ b/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/TimePicker/styled.ts @@ -1,6 +1,6 @@ import styled from "styled-components"; import { darken } from "@core/util/color.utils"; -import { Divider } from "@web/components/Divider"; +import { Divider } from "@web/components/Divider/Divider"; export interface Props { bgColor?: string; diff --git a/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/styled.ts b/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/styled.ts index e02eac329..fd2310d14 100644 --- a/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/styled.ts +++ b/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/styled.ts @@ -1,5 +1,5 @@ import styled from "styled-components"; -import { Flex } from "@web/components/Flex"; +import { Flex } from "@web/components/Flex/Flex"; export const StyledDateTimeFlex = styled(Flex)` gap: ${({ theme }) => theme.spacing.s}; diff --git a/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/RecurrenceSection.tsx b/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/RecurrenceSection.tsx index f6e915002..3f517bc29 100644 --- a/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/RecurrenceSection.tsx +++ b/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/RecurrenceSection.tsx @@ -6,7 +6,7 @@ import { useSession } from "@web/auth/compass/session/useSession"; import { isBackendUnavailable as getIsBackendUnavailable } from "@web/common/apis/util/backend-unavailable-error.util"; import { hoverColorByPriority } from "@web/common/styles/theme.util"; import { ConditionalRender } from "@web/components/ConditionalRender/ConditionalRender"; -import { FlexDirections } from "@web/components/Flex/styled"; +import { FlexDirections } from "@web/components/Flex/Flex"; import { EndsOnDate } from "@web/views/Forms/EventForm/DateControlsSection/RecurrenceSection/components/EndsOnDate"; import { RecurrenceIntervalSelect } from "@web/views/Forms/EventForm/DateControlsSection/RecurrenceSection/components/RecurrenceIntervalSelect"; import { RecurrenceToggle } from "@web/views/Forms/EventForm/DateControlsSection/RecurrenceSection/components/RecurrenceToggle"; diff --git a/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/components/EndsOnDate.tsx b/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/components/EndsOnDate.tsx index 0159e059e..4d9b844e3 100644 --- a/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/components/EndsOnDate.tsx +++ b/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/components/EndsOnDate.tsx @@ -4,8 +4,8 @@ import { darken } from "@core/util/color.utils"; import { parseCompassEventDate } from "@core/util/event/event.util"; import { theme } from "@web/common/styles/theme"; import { DatePicker } from "@web/components/DatePicker/DatePicker"; -import { Flex } from "@web/components/Flex"; -import { StyledText } from "@web/components/Text/styled"; +import { Flex } from "@web/components/Flex/Flex"; +import { Text } from "@web/components/Text/Text"; import { TooltipWrapper } from "@web/components/Tooltip/TooltipWrapper"; import { StyledRepeatRow } from "@web/views/Forms/EventForm/DateControlsSection/RecurrenceSection/styled"; @@ -29,7 +29,7 @@ export const EndsOnDate = ({ return ( - Ends on: + Ends on: - Every + Every = ({ }) => { return ( - On: + On: {WEEKDAYS.map((day) => ( = ({ } > - = ({ aria-keyshortcuts="Meta+Enter" > {saveText} - + ); diff --git a/packages/web/src/views/Forms/EventForm/styled.ts b/packages/web/src/views/Forms/EventForm/styled.ts index 61db2251a..46ca79908 100644 --- a/packages/web/src/views/Forms/EventForm/styled.ts +++ b/packages/web/src/views/Forms/EventForm/styled.ts @@ -1,10 +1,10 @@ import styled from "styled-components"; import { ZIndex } from "@web/common/constants/web.constants"; import { hoverColorByPriority } from "@web/common/styles/theme.util"; -import { PriorityButton } from "@web/components/Button/styled"; -import { Flex } from "@web/components/Flex"; +import { PriorityButton } from "@web/components/Button/Button"; +import { Flex } from "@web/components/Flex/Flex"; import { Input } from "@web/components/Input/Input"; -import { Textarea } from "@web/components/Textarea"; +import { Textarea } from "@web/components/Textarea/Textarea"; import { EVENT_WIDTH_MINIMUM } from "@web/views/Week/layout.constants"; import { type StyledFormProps } from "./types"; diff --git a/packages/web/src/views/NotFound/NotFound.tsx b/packages/web/src/views/NotFound/NotFound.tsx index 1037850dd..7f7097c5f 100644 --- a/packages/web/src/views/NotFound/NotFound.tsx +++ b/packages/web/src/views/NotFound/NotFound.tsx @@ -1,6 +1,6 @@ import { useNavigate } from "react-router-dom"; import { ROOT_ROUTES } from "@web/common/constants/routes"; -import { Text } from "@web/components/Text"; +import { Text } from "@web/components/Text/Text"; import { StyledBackButton, StyledNotFoundContainer } from "./styled"; export const NotFoundView = () => { diff --git a/packages/web/src/views/Week/components/Grid/AllDayRow/styled.ts b/packages/web/src/views/Week/components/Grid/AllDayRow/styled.ts index 8753f1a52..8ff0a4766 100644 --- a/packages/web/src/views/Week/components/Grid/AllDayRow/styled.ts +++ b/packages/web/src/views/Week/components/Grid/AllDayRow/styled.ts @@ -1,5 +1,5 @@ import styled from "styled-components"; -import { Flex } from "@web/components/Flex"; +import { Flex } from "@web/components/Flex/Flex"; import { GRID_MARGIN_LEFT, GRID_PADDING_BOTTOM, diff --git a/packages/web/src/views/Week/components/Header/DayLabels.tsx b/packages/web/src/views/Week/components/Header/DayLabels.tsx index a2beead01..d56e9b4f5 100644 --- a/packages/web/src/views/Week/components/Header/DayLabels.tsx +++ b/packages/web/src/views/Week/components/Header/DayLabels.tsx @@ -3,7 +3,7 @@ import styled from "styled-components"; import { type Dayjs } from "@core/util/date/dayjs"; import { theme } from "@web/common/styles/theme"; import { getWeekDayLabel } from "@web/common/utils/event/event.util"; -import { Text } from "@web/components/Text"; +import { Text } from "@web/components/Text/Text"; import { Columns } from "../Grid/Columns/styled"; interface Props { diff --git a/packages/web/src/views/Week/components/Header/Header.tsx b/packages/web/src/views/Week/components/Header/Header.tsx index 2469895ae..450b71f89 100644 --- a/packages/web/src/views/Week/components/Header/Header.tsx +++ b/packages/web/src/views/Week/components/Header/Header.tsx @@ -5,7 +5,7 @@ import { getCalendarHeadingLabel } from "@web/common/utils/datetime/web.date.uti import { HeaderInfoIcon } from "@web/components/HeaderInfoIcon/HeaderInfoIcon"; import { SidebarIcon } from "@web/components/Icons/Sidebar"; import { SelectView } from "@web/components/SelectView/SelectView"; -import { Text } from "@web/components/Text"; +import { Text } from "@web/components/Text/Text"; import { TooltipWrapper } from "@web/components/Tooltip/TooltipWrapper"; import { selectIsSidebarOpen } from "@web/ducks/events/selectors/view.selectors"; import { viewSlice } from "@web/ducks/events/slices/view.slice"; diff --git a/packages/web/src/views/Week/styled.tsx b/packages/web/src/views/Week/styled.tsx index 129b9642d..d4365ac37 100644 --- a/packages/web/src/views/Week/styled.tsx +++ b/packages/web/src/views/Week/styled.tsx @@ -1,7 +1,7 @@ import { type PropsWithChildren } from "react"; import styled from "styled-components"; import { ID_MAIN } from "@web/common/constants/web.constants"; -import { Flex } from "@web/components/Flex"; +import { Flex } from "@web/components/Flex/Flex"; import { WEEK_GRID_TRACK_MIN_WIDTH } from "./layout.constants"; export const Styled = styled(Flex)` From cb39b4ae21d5ba671bea76eab7a7c397fe83da0d Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Thu, 18 Jun 2026 17:18:28 -0500 Subject: [PATCH 04/29] refactor(web): migrate shared composites to tailwind --- .../common/styles/composites-tailwind.test.ts | 41 +++ .../web/src/common/styles/theme-css.test.ts | 8 + .../AbsoluteOverflowLoader.tsx | 9 +- .../AbsoluteOverflowLoader/styled.ts | 43 --- .../components/ContextMenu/ContextMenu.tsx | 23 +- .../ContextMenu/ContextMenuItems.tsx | 48 ++-- .../web/src/components/ContextMenu/styled.ts | 99 ------- .../src/components/DatePicker/DatePicker.tsx | 67 ++--- .../web/src/components/DatePicker/styled.ts | 171 ----------- .../LoginAbsoluteOverflowLoader.tsx | 20 +- .../LoginAbsoluteOverflowLoader/styled.ts | 104 ------- packages/web/src/index.css | 268 ++++++++++++++++++ .../PrioritySection/PrioritySection.tsx | 74 ++--- packages/web/src/views/NotFound/NotFound.tsx | 13 +- packages/web/src/views/NotFound/styled.ts | 38 --- 15 files changed, 438 insertions(+), 588 deletions(-) create mode 100644 packages/web/src/common/styles/composites-tailwind.test.ts delete mode 100644 packages/web/src/components/AbsoluteOverflowLoader/styled.ts delete mode 100644 packages/web/src/components/ContextMenu/styled.ts delete mode 100644 packages/web/src/components/DatePicker/styled.ts delete mode 100644 packages/web/src/components/LoginAbsoluteOverflowLoader/styled.ts delete mode 100644 packages/web/src/views/NotFound/styled.ts diff --git a/packages/web/src/common/styles/composites-tailwind.test.ts b/packages/web/src/common/styles/composites-tailwind.test.ts new file mode 100644 index 000000000..f8ab3f6d3 --- /dev/null +++ b/packages/web/src/common/styles/composites-tailwind.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from "bun:test"; +import { existsSync, readFileSync } from "node:fs"; +import { join } from "node:path"; + +const webSrc = join(import.meta.dir, "../.."); + +const migratedFiles = [ + "components/AbsoluteOverflowLoader/AbsoluteOverflowLoader.tsx", + "components/LoginAbsoluteOverflowLoader/LoginAbsoluteOverflowLoader.tsx", + "components/ContextMenu/ContextMenu.tsx", + "components/ContextMenu/ContextMenuItems.tsx", + "components/DatePicker/DatePicker.tsx", + "views/NotFound/NotFound.tsx", +] as const; + +const removedFiles = [ + "components/AbsoluteOverflowLoader/styled.ts", + "components/LoginAbsoluteOverflowLoader/styled.ts", + "components/ContextMenu/styled.ts", + "components/DatePicker/styled.ts", + "views/NotFound/styled.ts", +] as const; + +describe("Tailwind composite component architecture", () => { + it("keeps shared composite components independent from styled-components", () => { + for (const file of migratedFiles) { + const path = join(webSrc, file); + + expect(existsSync(path), `${file} should exist`).toBe(true); + expect(readFileSync(path, "utf8"), file).not.toContain( + "styled-components", + ); + } + + for (const file of removedFiles) { + expect(existsSync(join(webSrc, file)), `${file} should be removed`).toBe( + false, + ); + } + }); +}); diff --git a/packages/web/src/common/styles/theme-css.test.ts b/packages/web/src/common/styles/theme-css.test.ts index d4d63a138..3155eaa39 100644 --- a/packages/web/src/common/styles/theme-css.test.ts +++ b/packages/web/src/common/styles/theme-css.test.ts @@ -11,6 +11,8 @@ const semanticColorTokens = [ "border-primary", "border-primary-dark", "border-secondary", + "date-picker-outside-dark", + "date-picker-outside-light", "event-selected", "fg-primary", "fg-primary-dark", @@ -18,6 +20,12 @@ const semanticColorTokens = [ "gradient-accent-light-end", "grid-line-primary", "menu-bg", + "menu-border", + "menu-hover", + "menu-text", + "menu-tooltip-bg", + "menu-tooltip-text", + "not-found-gradient-highlight", "panel-badge-bg", "panel-bg", "panel-scrollbar", diff --git a/packages/web/src/components/AbsoluteOverflowLoader/AbsoluteOverflowLoader.tsx b/packages/web/src/components/AbsoluteOverflowLoader/AbsoluteOverflowLoader.tsx index 1b0169222..d3516d5da 100644 --- a/packages/web/src/components/AbsoluteOverflowLoader/AbsoluteOverflowLoader.tsx +++ b/packages/web/src/components/AbsoluteOverflowLoader/AbsoluteOverflowLoader.tsx @@ -1,16 +1,17 @@ import { AlignItems, + Flex, type FlexProps, JustifyContent, } from "@web/components/Flex/Flex"; -import { Styled, StyledSpinner } from "./styled"; export const AbsoluteOverflowLoader = (props: FlexProps) => ( - - - +
+ ); diff --git a/packages/web/src/components/AbsoluteOverflowLoader/styled.ts b/packages/web/src/components/AbsoluteOverflowLoader/styled.ts deleted file mode 100644 index 6c15b10cf..000000000 --- a/packages/web/src/components/AbsoluteOverflowLoader/styled.ts +++ /dev/null @@ -1,43 +0,0 @@ -import styled, { keyframes } from "styled-components"; -import { Flex } from "@web/components/Flex/Flex"; - -export const Styled = styled(Flex)` - position: absolute; - width: 100%; - height: 100%; - top: 0; - left: 0; - backdrop-filter: blur(5px); -`; - -const spinnerAnimation = keyframes` - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } -`; - -export const StyledSpinner = styled.div` - margin: 60px auto; - font-size: 4px; - position: relative; - text-indent: -9999em; - border-top: 1.1em solid rgba(255, 255, 255, 0.2); - border-right: 1.1em solid rgba(255, 255, 255, 0.2); - border-bottom: 1.1em solid rgba(255, 255, 255, 0.2); - border-left: 1.1em solid #ffffff; - transform: translateZ(0); - animation: ${spinnerAnimation} 1.1s infinite linear; - - border-radius: 50%; - width: 10em; - height: 10em; - - &:after { - border-radius: 50%; - width: 10em; - height: 10em; - } -`; diff --git a/packages/web/src/components/ContextMenu/ContextMenu.tsx b/packages/web/src/components/ContextMenu/ContextMenu.tsx index 0833abbdc..407dfeb80 100644 --- a/packages/web/src/components/ContextMenu/ContextMenu.tsx +++ b/packages/web/src/components/ContextMenu/ContextMenu.tsx @@ -6,8 +6,6 @@ import { useRole, } from "@floating-ui/react"; import React from "react"; -import styled from "styled-components"; -import { ZIndex } from "@web/common/constants/web.constants"; import { type Schema_GridEvent } from "@web/common/types/web.event.types"; import { ContextMenuItems, @@ -15,18 +13,6 @@ import { ContextMenuItemsView, } from "./ContextMenuItems"; -const MenuWrapper = styled.ul` - position: absolute; - background-color: ${({ theme }) => theme.color.menu.bg}; - border: ${({ theme }) => `1px solid ${theme.color.border.primary}`}; - box-shadow: ${({ theme }) => `0px 4px 6px ${theme.color.shadow.default}`}; - border-radius: ${({ theme }) => theme.shape.borderRadius}; - padding: 5px 0; - list-style: none; - z-index: ${ZIndex.LAYER_2}; - min-width: 160px; -`; - interface ContextMenuProps { actions?: ContextMenuItemsActions; event?: Schema_GridEvent; @@ -67,7 +53,12 @@ export const ContextMenu = React.forwardRef( if (!event) return null; return ( - +
    {actions ? ( ( ) : ( )} - +
); }, ); diff --git a/packages/web/src/components/ContextMenu/ContextMenuItems.tsx b/packages/web/src/components/ContextMenu/ContextMenuItems.tsx index 236b25880..5d6029831 100644 --- a/packages/web/src/components/ContextMenu/ContextMenuItems.tsx +++ b/packages/web/src/components/ContextMenu/ContextMenuItems.tsx @@ -2,18 +2,11 @@ import { Copy, PenNib, Trash } from "@phosphor-icons/react"; import type React from "react"; import { Priorities } from "@core/constants/core.constants"; import { ID_CONTEXT_MENU_ITEMS } from "@web/common/constants/web.constants"; +import { type CSSVariables } from "@web/common/styles/css.types"; import { colorByPriority } from "@web/common/styles/theme.util"; import { type Schema_GridEvent } from "@web/common/types/web.event.types"; import { assembleGridEvent } from "@web/common/utils/event/event.util"; import { getSomedayEventCategory } from "@web/common/utils/event/someday.event.util"; -import { - MenuItem, - MenuItemLabel, - PriorityCircle, - PriorityContainer, - TooltipText, - TooltipWrapper, -} from "@web/components/ContextMenu/styled"; import { useSidebarContext } from "@web/components/PlannerSidebar/draft/context/useSidebarContext"; import { selectIsEventPending } from "@web/ducks/events/selectors/pending.selectors"; import { useAppSelector } from "@web/store/store.hooks"; @@ -108,31 +101,37 @@ export function ContextMenuItemsView({ return (
- +
{priorities.map((priority) => ( - - +
))} -
+
{menuActions.map((item) => { const disabled = isActionDisabled(item.id); return ( - {item.icon} - {item.label} - + {item.label} + ); })}
diff --git a/packages/web/src/components/ContextMenu/styled.ts b/packages/web/src/components/ContextMenu/styled.ts deleted file mode 100644 index 592447193..000000000 --- a/packages/web/src/components/ContextMenu/styled.ts +++ /dev/null @@ -1,99 +0,0 @@ -import styled from "styled-components"; - -export const PriorityContainer = styled.div` - display: flex; - justify-content: center; - gap: 10px; - padding: 10px; -`; - -export const TooltipWrapper = styled.div` - position: relative; - display: flex; - flex-direction: column; - align-items: center; -`; - -export const TooltipText = styled.span` - position: absolute; - bottom: 100%; - margin-bottom: 6px; - padding: 4px 8px; - background-color: #333; - color: #fff; - border-radius: 4px; - white-space: nowrap; - font-size: 13px; - pointer-events: none; - opacity: 0; - visibility: hidden; - transform: translateY(5px); - transition: all 0.2s ease-in-out; - z-index: 10; - - ${TooltipWrapper}:hover & { - opacity: 1; - visibility: visible; - transform: translateY(0); - } -`; - -export const PriorityCircle = styled.div<{ color: string; selected: boolean }>` - appearance: none; - padding: 0; - width: 20px; - height: 20px; - border-radius: 50%; - border: 2px solid ${({ color }) => color}; - background-color: ${({ selected, color }) => - selected ? color : "transparent"}; - cursor: pointer; - transition: ${({ theme }) => theme.transition.default}; - - &:hover { - ${({ selected, theme }) => - selected - ? `filter: brightness(85%);` - : `background-color: ${theme.color.border.primary};`} - } - - &:focus { - outline: none; - box-shadow: 0 0 0 2px ${({ theme }) => theme.color.border.primaryDark}; - } -`; - -export const MenuItem = styled.button` - appearance: none; - background: transparent; - border: 0; - border-bottom: 1px solid #eee; - width: 100%; - padding: 10px 12px; - cursor: pointer; - user-select: none; - font-size: 14px; - color: #333; - white-space: nowrap; - display: flex; - align-items: center; - gap: 8px; - text-align: left; - - &:last-child { - border-bottom: none; - } - - &:hover { - background-color: #f5f5f5; - } - - &:disabled { - cursor: wait; - opacity: 0.5; - } -`; - -export const MenuItemLabel = styled.span` - font-size: ${({ theme }) => theme.text.size.l}; -`; diff --git a/packages/web/src/components/DatePicker/DatePicker.tsx b/packages/web/src/components/DatePicker/DatePicker.tsx index 396640a71..0aa3e0005 100644 --- a/packages/web/src/components/DatePicker/DatePicker.tsx +++ b/packages/web/src/components/DatePicker/DatePicker.tsx @@ -4,16 +4,10 @@ import * as ReactDatePickerModule from "react-datepicker"; import { type ReactDatePickerProps } from "react-datepicker"; import { darken, isDark } from "@core/util/color.utils"; import dayjs from "@core/util/date/dayjs"; +import { type CSSVariables } from "@web/common/styles/css.types"; import { theme } from "@web/common/styles/theme"; import { resolveDefaultExport } from "@web/common/utils/resolve-default-export.util"; import { MonthNavButton } from "@web/components/DatePicker/MonthNavButton"; -import { - ChangeDayButtonsStyledFlex, - MonthContainerStyled, - StyledDatePicker, - StyledHeaderFlex, - TodayStyledText, -} from "@web/components/DatePicker/styled"; import { AlignItems, Flex, JustifyContent } from "@web/components/Flex/Flex"; import { InputBase } from "@web/components/Input/Input"; import { Text } from "@web/components/Text/Text"; @@ -44,7 +38,7 @@ const ReactDatePicker = resolveDefaultExport( export const DatePicker: React.FC = (datePickerProps) => { const { animationOnToggle = true, - bgColor = theme.color.bg.primary, + bgColor, calendarClassName, headerActionsClassName, headerClassName, @@ -58,12 +52,17 @@ export const DatePicker: React.FC = (datePickerProps) => { withTodayButton = true, ...props } = datePickerProps; + const resolvedBgColor = bgColor ?? theme.color.bg.primary; + const datePickerStyle: CSSVariables = { + "--date-picker-bg": bgColor ?? "var(--compass-color-bg-primary)", + }; + const isDarkBackground = isDark(resolvedBgColor); const headerColor = view === "sidebar" - ? theme.color.text.light - : isDark(bgColor || "") - ? theme.color.text.lighter - : theme.color.text.dark; + ? "var(--compass-color-text-light)" + : isDarkBackground + ? "var(--compass-color-text-lighter)" + : "var(--compass-color-text-dark)"; return ( = (datePickerProps) => { "calendar--open": isOpen, "calendar--animation": animationOnToggle, })} - calendarContainer={(containerProps) => ( - + calendarContainer={({ children, className }) => ( +
+ {children} +
)} customInput={ @@ -115,12 +115,12 @@ export const DatePicker: React.FC = (datePickerProps) => { const currentMonth = dayjs().format("MMM YYYY"); return ( - - + = (datePickerProps) => { > {selectedMonth} - +
{!customHeaderCount && ( - + = (datePickerProps) => { > - + {withTodayButton && ( - { headerProps.changeMonth(dayjs().month()); headerProps.changeYear(dayjs().year()); }} - color={theme.color.text.light} + color="var(--compass-color-text-light)" size="l" > Today - + )} )} @@ -178,7 +181,7 @@ export const DatePicker: React.FC = (datePickerProps) => { {headerEndContent}
) : null} - + ); }} /> diff --git a/packages/web/src/components/DatePicker/styled.ts b/packages/web/src/components/DatePicker/styled.ts deleted file mode 100644 index 7feabadf0..000000000 --- a/packages/web/src/components/DatePicker/styled.ts +++ /dev/null @@ -1,171 +0,0 @@ -import styled from "styled-components"; -import { brighten, darken, isDark } from "@core/util/color.utils"; -import { theme } from "@web/common/styles/theme"; -import { Flex } from "@web/components/Flex/Flex"; -import { Text } from "@web/components/Text/Text"; -import { SIDEBAR_MONTH_HEIGHT } from "@web/views/Week/layout.constants"; - -const SIDEBAR_COMPACT_MONTH_HEIGHT = 252; - -const _hoverStyle = ` - background-color: ${theme.color.fg.primary}; - color: ${theme.color.text.dark}; -`; - -export const ChangeDayButtonsStyledFlex = styled(Flex)` - gap: 4px; -`; - -export const MonthContainerStyled = styled(Flex)` - width: 97px; -`; - -interface Props { - bgColor: string; - isDark?: boolean; - selectedColor: string; - view: "grid" | "sidebar"; -} - -export const StyledDatePicker = styled.div.attrs((props) => ({ - bgColor: props.bgColor, - isDark: isDark(props.bgColor), - selectedColor: props.selectedColor, - view: props.view, -}))` - background-color: ${({ bgColor, view }) => - view === "sidebar" ? "transparent" : bgColor}; - border: none; - border-radius: 2px; - box-shadow: 0px 4px 4px ${({ theme }) => theme.color.shadow.default}; - font-weight: 500; - font-size: ${({ view }) => (view === "sidebar" ? "11px" : "12px")}; - user-select: none; - - & .react-datepicker { - &__month-container { - width: 100%; - padding: ${({ view }) => (view === "sidebar" ? "0" : "0 15px")}; - height: ${({ view }) => - view === "sidebar" ? `${SIDEBAR_COMPACT_MONTH_HEIGHT}px` : "285px"}; - display: flex; - flex-direction: column; - } - - &__month { - margin: 0; - display: flex; - flex-direction: column; - flex: 1; - } - - &__week { - align-items: center; - display: flex; - justify-content: space-between; - flex-basis: calc(100% / 6); - width: 100%; - } - - &__header { - align-items: left; - background: unset; - border-bottom: none; - padding: ${({ view }) => - view === "sidebar" ? "8px 0 0" : "8px 0 0 2px"}; - } - - &__day-name { - opacity: 0.8; - color: ${({ theme, isDark }) => - isDark ? theme.color.text.light : theme.color.text.dark}; - font-size: ${({ view }) => (view === "sidebar" ? "10px" : "11px")}; - margin: 0; - } - - &__day { - border: none !important; - border-radius: 50% !important; - color: ${({ theme, isDark }) => - isDark ? theme.color.text.lighter : theme.color.text.dark}; - height: ${({ view }) => (view === "sidebar" ? "1.55rem" : "1.7rem")}; - line-height: ${({ view }) => (view === "sidebar" ? "1.55rem" : "1.7rem")}; - margin: 0; - width: ${({ view }) => (view === "sidebar" ? "1.55rem" : "1.7rem")}; - - &:hover { - ${_hoverStyle} - } - - &-names { - display: flex; - justify-content: space-between; - align-items: center; - width: 100%; - margin-bottom: 6px; - } - - &--selected { - background-color: ${({ selectedColor }) => selectedColor}; - color: ${({ theme }) => theme.color.text.lighter}; - } - - &--today:not(.react-datepicker__day--selected) { - color: ${({ view }) => - view === "sidebar" ? theme.color.text.accent : theme.color.text.dark}; - text-decoration: underline; - text-decoration-color: ${({ view }) => - view === "sidebar" ? theme.color.text.accent : theme.color.text.dark}; - text-underline-offset: 3px; - - &:hover { - color: ${({ theme }) => theme.color.text.dark}; - } - } - - &--keyboard-selected { - background-color: ${({ view }) => - view === "sidebar" ? "transparent" : theme.color.bg.primary}; - } - - &--outside-month { - color: ${({ theme, isDark }) => - isDark - ? darken(theme.color.text.light) - : brighten(theme.color.text.dark)}; - opacity: 0.8; - } - } - - &.calendar { - height: 0; - width: 414px; - overflow: hidden; - - &--open { - height: ${({ view }) => - view === "sidebar" - ? `${SIDEBAR_COMPACT_MONTH_HEIGHT}px` - : `${SIDEBAR_MONTH_HEIGHT}px`}; - } - - &--animation { - transition: 0.3s; - } - } - } -`; - -export const StyledHeaderFlex = styled(Flex)` - padding: 0 5px 5px 8px; -`; -export const TodayStyledText = styled(Text)<{ isCurrentDate: boolean }>` - margin-right: 40px; - padding: 0px 6px; - opacity: ${({ isCurrentDate }) => (isCurrentDate ? 0 : 1)}; - - &:hover { - filter: brightness(160%); - transition: filter 0.35s ease-out; - } -`; diff --git a/packages/web/src/components/LoginAbsoluteOverflowLoader/LoginAbsoluteOverflowLoader.tsx b/packages/web/src/components/LoginAbsoluteOverflowLoader/LoginAbsoluteOverflowLoader.tsx index 9d7e1da40..c77f10ba7 100644 --- a/packages/web/src/components/LoginAbsoluteOverflowLoader/LoginAbsoluteOverflowLoader.tsx +++ b/packages/web/src/components/LoginAbsoluteOverflowLoader/LoginAbsoluteOverflowLoader.tsx @@ -1,10 +1,10 @@ import { useEffect, useState } from "react"; import { AlignItems, + Flex, type FlexProps, JustifyContent, } from "@web/components/Flex/Flex"; -import { LoadingMessage, ProgressBar, Styled, StyledSpinner } from "./styled"; /** * Absolute overflow loader customized to handle login flow @@ -29,19 +29,23 @@ export const LoginAbsoluteOverflowLoader = (props: FlexProps) => { }, []); return ( - - +
{showMessage && ( - -
{currentMessage}
- -
+
+
{currentMessage}
+
+
)} - + ); }; diff --git a/packages/web/src/components/LoginAbsoluteOverflowLoader/styled.ts b/packages/web/src/components/LoginAbsoluteOverflowLoader/styled.ts deleted file mode 100644 index 4ead29a41..000000000 --- a/packages/web/src/components/LoginAbsoluteOverflowLoader/styled.ts +++ /dev/null @@ -1,104 +0,0 @@ -import styled, { keyframes } from "styled-components"; -import { c } from "@web/common/styles/colors"; -import { Flex } from "@web/components/Flex/Flex"; - -export const Styled = styled(Flex)` - position: absolute; - width: 100%; - height: 100%; - top: 0; - left: 0; - backdrop-filter: blur(5px); -`; - -const spinnerAnimation = keyframes` - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } -`; - -export const StyledSpinner = styled.div` - margin: 60px auto; - font-size: 4px; - position: relative; - text-indent: -9999em; - border-top: 1.1em solid rgba(255, 255, 255, 0.2); - border-right: 1.1em solid rgba(255, 255, 255, 0.2); - border-bottom: 1.1em solid rgba(255, 255, 255, 0.2); - border-left: 1.1em solid #ffffff; - transform: translateZ(0); - animation: ${spinnerAnimation} 1.1s infinite linear; - - border-radius: 50%; - width: 10em; - height: 10em; - - &:after { - border-radius: 50%; - width: 10em; - height: 10em; - } -`; - -const progressAnimation = keyframes` - 0% { - left: -35%; - right: 100%; - } - 60% { - left: 100%; - right: -90%; - } - 100% { - left: 100%; - right: -90%; - } -`; - -const fadeIn = keyframes` - from { - opacity: 0; - } - to { - opacity: 1; - } -`; - -export const LoadingMessage = styled.div` - position: absolute; - bottom: 40%; - left: 50%; - transform: translateX(-50%); - color: ${c.white100}; - font-size: 16px; - text-align: center; - width: 100%; - max-width: 400px; - animation: ${fadeIn} 0.8s ease-in-out; - - div { - margin-bottom: 15px; - } -`; - -export const ProgressBar = styled.div` - width: 100%; - height: 3px; - background-color: ${c.gray800}; - border-radius: 3px; - overflow: hidden; - position: relative; - - &:after { - content: ""; - position: absolute; - top: 0; - height: 100%; - background: linear-gradient(90deg, ${c.blue100}, ${c.purple}); - animation: ${progressAnimation} 1.5s ease-in-out infinite; - width: 45%; - } -`; diff --git a/packages/web/src/index.css b/packages/web/src/index.css index 1c14c1f63..0b9d0af55 100644 --- a/packages/web/src/index.css +++ b/packages/web/src/index.css @@ -15,12 +15,20 @@ --compass-color-border-primary-dark: hsl(0 0 0 / 50.2%); --compass-color-border-secondary: hsl(47 7 73); --compass-color-event-selected: hsl(206 18 72); + --compass-color-date-picker-outside-dark: hsl(47 7 63); + --compass-color-date-picker-outside-light: hsl(222 12 17); --compass-color-fg-primary: hsl(47 7 73); --compass-color-fg-primary-dark: hsl(208 13 71 / 54.9%); --compass-color-gradient-accent-light-start: hsl(202 100 67); --compass-color-gradient-accent-light-end: hsl(196 45 78); --compass-color-grid-line-primary: hsl(219 18 34 / 20%); --compass-color-menu-bg: hsl(0 0 98); + --compass-color-menu-border: hsl(0 0 93); + --compass-color-menu-hover: hsl(0 0 96); + --compass-color-menu-text: hsl(0 0 20); + --compass-color-menu-tooltip-bg: hsl(0 0 20); + --compass-color-menu-tooltip-text: hsl(0 0 100); + --compass-color-not-found-gradient-highlight: hsl(218 27 8); --compass-color-panel-badge-bg: hsl(0 0 100 / 7%); --compass-color-panel-bg: hsl(219 8 46 / 20%); --compass-color-panel-scrollbar: hsl(219 8 46 / 20%); @@ -53,6 +61,12 @@ --color-border-primary-dark: var(--compass-color-border-primary-dark); --color-border-secondary: var(--compass-color-border-secondary); --color-event-selected: var(--compass-color-event-selected); + --color-date-picker-outside-dark: var( + --compass-color-date-picker-outside-dark + ); + --color-date-picker-outside-light: var( + --compass-color-date-picker-outside-light + ); --color-fg-primary: var(--compass-color-fg-primary); --color-fg-primary-dark: var(--compass-color-fg-primary-dark); --color-gradient-accent-light-start: var( @@ -63,6 +77,14 @@ ); --color-grid-line-primary: var(--compass-color-grid-line-primary); --color-menu-bg: var(--compass-color-menu-bg); + --color-menu-border: var(--compass-color-menu-border); + --color-menu-hover: var(--compass-color-menu-hover); + --color-menu-text: var(--compass-color-menu-text); + --color-menu-tooltip-bg: var(--compass-color-menu-tooltip-bg); + --color-menu-tooltip-text: var(--compass-color-menu-tooltip-text); + --color-not-found-gradient-highlight: var( + --compass-color-not-found-gradient-highlight + ); --color-panel-badge-bg: var(--compass-color-panel-badge-bg); --color-panel-bg: var(--compass-color-panel-bg); --color-panel-scrollbar: var(--compass-color-panel-scrollbar); @@ -139,6 +161,9 @@ cubic-bezier(0.16, 1, 0.3, 1) both; --animate-someday-cold-fade-in: somedayColdFadeIn 600ms cubic-bezier(0.16, 1, 0.3, 1) both; + --animate-loader-spin: loaderSpin 1.1s linear infinite; + --animate-login-progress: loginProgress 1.5s ease-in-out infinite; + --animate-login-message-in: loginMessageIn 0.8s ease-in-out; --animate-spinner-rotate: spinnerRotate 1.1s linear infinite; @keyframes progressSlide { @@ -211,6 +236,36 @@ transform: rotate(360deg); } } + + @keyframes loaderSpin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } + } + + @keyframes loginProgress { + 0% { + right: 100%; + left: -35%; + } + 60%, + 100% { + right: -90%; + left: 100%; + } + } + + @keyframes loginMessageIn { + from { + opacity: 0; + } + to { + opacity: 1; + } + } } @media (prefers-reduced-motion: reduce) { @@ -325,6 +380,219 @@ } } +@utility c-overflow-loader { + @apply absolute inset-0 h-full w-full backdrop-blur-[5px]; +} + +@utility c-loader-spinner { + @apply relative my-15 h-[10em] w-[10em] animate-loader-spin rounded-full border-[1.1em] border-white/20 border-l-text-lighter text-[4px] -indent-[9999em]; + transform: translateZ(0); +} + +@utility c-login-progress { + @apply relative h-[3px] w-full overflow-hidden rounded-[3px] bg-border-primary; + + &::after { + @apply absolute top-0 h-full w-[45%] animate-login-progress content-[""]; + background: linear-gradient( + 90deg, + var(--compass-color-accent-primary), + var(--compass-color-tag-three) + ); + } +} + +@utility c-context-menu { + @apply absolute z-2 min-w-40 list-none rounded border border-border-primary bg-menu-bg py-[5px] shadow-[0_4px_6px_var(--color-shadow-default)]; +} + +@utility c-context-priority-circle { + @apply h-5 w-5 cursor-pointer appearance-none rounded-full border-2 border-(--priority-color) bg-transparent p-0 transition-[background-color,box-shadow,filter] duration-300 hover:bg-border-primary focus:outline-none focus:shadow-[0_0_0_2px_var(--color-border-primary-dark)] disabled:cursor-wait; + + &[data-selected="true"] { + @apply bg-(--priority-color) hover:brightness-85; + } +} + +@utility c-context-tooltip { + @apply pointer-events-none invisible absolute bottom-full z-10 mb-1.5 translate-y-[5px] whitespace-nowrap rounded bg-menu-tooltip-bg px-2 py-1 text-[13px] text-menu-tooltip-text opacity-0 transition-all duration-200 ease-in-out group-hover:visible group-hover:translate-y-0 group-hover:opacity-100; +} + +@utility c-context-menu-item { + @apply flex w-full cursor-pointer appearance-none items-center gap-2 whitespace-nowrap border-0 border-menu-border border-b bg-transparent px-3 py-2.5 text-left text-[14px] text-menu-text select-none last:border-b-0 hover:bg-menu-hover disabled:cursor-wait disabled:opacity-50; +} + +@utility c-not-found { + @apply flex h-screen w-screen flex-col items-center justify-center text-text-lighter; + background: linear-gradient( + to right, + var(--compass-color-bg-primary), + var(--compass-color-bg-primary), + var(--compass-color-bg-secondary), + var(--compass-color-not-found-gradient-highlight), + var(--compass-color-bg-primary) + ); +} + +@utility c-not-found-back-button { + @apply mt-5 mb-5 cursor-pointer rounded border-2 border-border-primary bg-fg-primary-dark px-4 py-2 font-semibold text-[16px] text-text-lighter transition-all duration-200 ease-in-out hover:brightness-120; +} + +@utility c-date-picker { + @apply select-none rounded-[2px] border-0 font-medium shadow-[0_4px_4px_var(--color-shadow-default)]; + background-color: var(--date-picker-bg); + font-size: 12px; + + &[data-view="sidebar"] { + background-color: transparent; + font-size: 11px; + } + + & .react-datepicker__month-container { + display: flex; + width: 100%; + height: 285px; + flex-direction: column; + padding: 0 15px; + } + + &[data-view="sidebar"] .react-datepicker__month-container { + height: 252px; + padding: 0; + } + + & .react-datepicker__month { + display: flex; + flex: 1; + flex-direction: column; + margin: 0; + } + + & .react-datepicker__week { + display: flex; + width: 100%; + flex-basis: calc(100% / 6); + align-items: center; + justify-content: space-between; + } + + & .react-datepicker__header { + align-items: start; + border-bottom: 0; + background: unset; + padding: 8px 0 0 2px; + } + + &[data-view="sidebar"] .react-datepicker__header { + padding: 8px 0 0; + } + + & .react-datepicker__day-name { + margin: 0; + color: var(--compass-color-text-dark); + font-size: 11px; + opacity: 0.8; + } + + &[data-dark="true"] .react-datepicker__day-name { + color: var(--compass-color-text-light); + } + + &[data-view="sidebar"] .react-datepicker__day-name { + font-size: 10px; + } + + & .react-datepicker__day { + width: 1.7rem; + height: 1.7rem; + margin: 0; + border: 0; + border-radius: 50%; + color: var(--compass-color-text-dark); + line-height: 1.7rem; + } + + &[data-dark="true"] .react-datepicker__day { + color: var(--compass-color-text-lighter); + } + + &[data-view="sidebar"] .react-datepicker__day { + width: 1.55rem; + height: 1.55rem; + line-height: 1.55rem; + } + + & .react-datepicker__day:hover { + background-color: var(--compass-color-fg-primary); + color: var(--compass-color-text-dark); + } + + & .react-datepicker__day-names { + display: flex; + width: 100%; + align-items: center; + justify-content: space-between; + margin-bottom: 6px; + } + + & .react-datepicker__day--selected { + background-color: var(--compass-color-accent-primary); + color: var(--compass-color-text-lighter); + } + + & .react-datepicker__day--today:not(.react-datepicker__day--selected) { + color: var(--compass-color-text-dark); + text-decoration: underline; + text-decoration-color: var(--compass-color-text-dark); + text-underline-offset: 3px; + } + + &[data-view="sidebar"] + .react-datepicker__day--today:not(.react-datepicker__day--selected) { + color: var(--compass-color-accent-primary); + text-decoration-color: var(--compass-color-accent-primary); + } + + & .react-datepicker__day--today:hover { + color: var(--compass-color-text-dark); + } + + & .react-datepicker__day--keyboard-selected { + background-color: var(--compass-color-bg-primary); + } + + &[data-view="sidebar"] .react-datepicker__day--keyboard-selected { + background-color: transparent; + } + + & .react-datepicker__day--outside-month { + color: var(--compass-color-date-picker-outside-light); + opacity: 0.8; + } + + &[data-dark="true"] .react-datepicker__day--outside-month { + color: var(--compass-color-date-picker-outside-dark); + } + + &.calendar { + width: 414px; + height: 0; + overflow: hidden; + } + + &.calendar--open { + height: 275px; + } + + &[data-view="sidebar"].calendar--open { + height: 252px; + } + + &.calendar--animation { + transition: 0.3s; + } +} + @utility compass-scroll { @layer components { :focus-visible { diff --git a/packages/web/src/views/Forms/EventForm/PrioritySection/PrioritySection.tsx b/packages/web/src/views/Forms/EventForm/PrioritySection/PrioritySection.tsx index 7cbc130d8..d4cd51a34 100644 --- a/packages/web/src/views/Forms/EventForm/PrioritySection/PrioritySection.tsx +++ b/packages/web/src/views/Forms/EventForm/PrioritySection/PrioritySection.tsx @@ -1,11 +1,7 @@ import type React from "react"; import { Priorities, type Priority } from "@core/constants/core.constants"; +import { type CSSVariables } from "@web/common/styles/css.types"; import { colorByPriority } from "@web/common/styles/theme.util"; -import { - PriorityCircle, - TooltipText, - TooltipWrapper, -} from "@web/components/ContextMenu/styled"; import { type SetEventFormField } from "../types"; import { StyledPriorityFlex } from "./styled"; @@ -18,47 +14,37 @@ export const PrioritySection: React.FC = ({ onSetEventField, priority, }) => { + const priorities = [ + { color: colorByPriority.work, label: "Work", value: Priorities.WORK }, + { color: colorByPriority.self, label: "Self", value: Priorities.SELF }, + { + color: colorByPriority.relationships, + label: "Relationships", + value: Priorities.RELATIONS, + }, + ]; + return ( - - { - onSetEventField({ priority: Priorities.WORK }); - }} - onFocus={() => onSetEventField({ priority: Priorities.WORK })} - role="tab" - tabIndex={0} - /> - Work - - - - onSetEventField({ priority: Priorities.SELF })} - onFocus={() => onSetEventField({ priority: Priorities.SELF })} - role="tab" - tabIndex={0} - /> - Self - - - - { - onSetEventField({ priority: Priorities.RELATIONS }); - }} - onFocus={() => onSetEventField({ priority: Priorities.RELATIONS })} - role="tab" - tabIndex={0} - /> - Relationships - + {priorities.map((item) => ( +
+
+ ))}
); }; diff --git a/packages/web/src/views/NotFound/NotFound.tsx b/packages/web/src/views/NotFound/NotFound.tsx index 7f7097c5f..5214cb0fb 100644 --- a/packages/web/src/views/NotFound/NotFound.tsx +++ b/packages/web/src/views/NotFound/NotFound.tsx @@ -1,7 +1,6 @@ import { useNavigate } from "react-router-dom"; import { ROOT_ROUTES } from "@web/common/constants/routes"; import { Text } from "@web/components/Text/Text"; -import { StyledBackButton, StyledNotFoundContainer } from "./styled"; export const NotFoundView = () => { const navigate = useNavigate(); @@ -9,7 +8,7 @@ export const NotFoundView = () => { const goHome = () => navigate(ROOT_ROUTES.ROOT); return ( - +
🏴‍☠️ Shiver me timbers!
@@ -18,9 +17,13 @@ export const NotFoundView = () => { This isn't part of the app, matey
- + +
); }; diff --git a/packages/web/src/views/NotFound/styled.ts b/packages/web/src/views/NotFound/styled.ts deleted file mode 100644 index 7a792248b..000000000 --- a/packages/web/src/views/NotFound/styled.ts +++ /dev/null @@ -1,38 +0,0 @@ -import styled from "styled-components"; -import { darkBlueGradient } from "@web/common/styles/theme.util"; - -export const StyledNotFoundContainer = styled.div` - align-items: center; - background: linear-gradient( - to right, - ${darkBlueGradient.level1}, - ${darkBlueGradient.level2}, - ${darkBlueGradient.level3}, - ${darkBlueGradient.level4}, - ${darkBlueGradient.level5} - ); - color: ${({ theme }) => theme.color.text.lighter}; - display: flex; - flex-direction: column; - height: 100vh; - justify-content: center; - width: 100vw; -`; - -export const StyledBackButton = styled.button` - background: ${({ theme }) => theme.color.fg.primaryDark}; - border: ${({ theme }) => `2px solid ${theme.color.border.primary}`}; - border-radius: ${({ theme }) => theme.shape.borderRadius}; - color: ${({ theme }) => theme.color.text.lighter}; - cursor: pointer; - font-size: 16px; - font-weight: 600; - padding: 8px 16px; - margin-bottom: 20px; - margin-top: 20px; - transition: all 0.2s ease-in-out; - - &:hover { - filter: brightness(120%); - } -`; From f5626533c8209b62b8281a9f6ecd5e68433a6a86 Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Thu, 18 Jun 2026 17:23:43 -0500 Subject: [PATCH 05/29] refactor(web): migrate event forms to tailwind --- .../src/common/styles/forms-tailwind.test.ts | 50 +++++ .../SomedayEventContainer.tsx | 6 +- packages/web/src/index.css | 176 ++++++++++++++++++ .../views/Forms/ActionsMenu/ActionsMenu.tsx | 16 +- .../src/views/Forms/ActionsMenu/MenuItem.tsx | 8 +- .../web/src/views/Forms/ActionsMenu/styled.ts | 56 ------ .../DateControlsSection.tsx | 5 +- .../DateControlsSection/styled.ts | 9 - .../DatePickers/DatePickers.tsx | 11 +- .../DateTimeSection/DatePickers/styled.ts | 6 - .../DateTimeSection/DateTimeSection.tsx | 7 +- .../DateTimeSection/TimePicker/TimePicker.tsx | 16 +- .../TimePicker/TimePickers.tsx | 7 +- .../DateTimeSection/TimePicker/styled.ts | 86 --------- .../DateTimeSection/styled.ts | 8 - .../RecurrenceSection/RecurrenceSection.tsx | 6 +- .../components/CaretInput.tsx | 20 +- .../components/EndsOnDate.tsx | 5 +- .../components/RecurrenceIntervalSelect.tsx | 14 +- .../components/RecurrenceToggle.tsx | 17 +- .../RecurrenceSection/components/WeekDay.tsx | 20 +- .../RecurrenceSection/components/WeekDays.tsx | 5 +- .../RecurrenceSection/styled.ts | 151 --------------- .../src/views/Forms/EventForm/EventForm.tsx | 31 +-- .../EventForm/MoveToSidebarMenuButton.tsx | 8 +- .../PrioritySection/PrioritySection.tsx | 6 +- .../Forms/EventForm/PrioritySection/styled.ts | 7 - .../EventForm/SaveSection/SaveSection.tsx | 6 +- .../web/src/views/Forms/EventForm/styled.ts | 71 ------- .../web/src/views/Forms/EventForm/types.ts | 5 - .../FloatingFormContainer.tsx | 28 +++ .../SomedayEventForm/SomedayEventForm.tsx | 36 ++-- .../SomedayRecurrenceSection.tsx | 16 +- .../SomedayRecurrenceSelect.tsx | 10 +- .../SomedayRecurrenceSelect/styled.ts | 10 - .../views/Forms/SomedayEventForm/styled.ts | 16 -- .../Week/components/Draft/grid/GridDraft.tsx | 6 +- 37 files changed, 395 insertions(+), 566 deletions(-) create mode 100644 packages/web/src/common/styles/forms-tailwind.test.ts delete mode 100644 packages/web/src/views/Forms/ActionsMenu/styled.ts delete mode 100644 packages/web/src/views/Forms/EventForm/DateControlsSection/DateControlsSection/styled.ts delete mode 100644 packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/DatePickers/styled.ts delete mode 100644 packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/TimePicker/styled.ts delete mode 100644 packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/styled.ts delete mode 100644 packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/styled.ts delete mode 100644 packages/web/src/views/Forms/EventForm/PrioritySection/styled.ts delete mode 100644 packages/web/src/views/Forms/EventForm/styled.ts create mode 100644 packages/web/src/views/Forms/SomedayEventForm/FloatingFormContainer.tsx delete mode 100644 packages/web/src/views/Forms/SomedayEventForm/SomedayRecurrenceSection/SomedayRecurrenceSelect/styled.ts delete mode 100644 packages/web/src/views/Forms/SomedayEventForm/styled.ts diff --git a/packages/web/src/common/styles/forms-tailwind.test.ts b/packages/web/src/common/styles/forms-tailwind.test.ts new file mode 100644 index 000000000..d0b67d6f3 --- /dev/null +++ b/packages/web/src/common/styles/forms-tailwind.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, test } from "bun:test"; +import { existsSync, readFileSync } from "node:fs"; +import { join } from "node:path"; + +const formsRoot = join(import.meta.dir, "../../views/Forms"); + +const productionFiles = [ + "ActionsMenu/ActionsMenu.tsx", + "ActionsMenu/MenuItem.tsx", + "EventForm/EventForm.tsx", + "EventForm/MoveToSidebarMenuButton.tsx", + "EventForm/SaveSection/SaveSection.tsx", + "EventForm/PrioritySection/PrioritySection.tsx", + "EventForm/DateControlsSection/DateControlsSection/DateControlsSection.tsx", + "EventForm/DateControlsSection/DateTimeSection/DateTimeSection.tsx", + "EventForm/DateControlsSection/DateTimeSection/DatePickers/DatePickers.tsx", + "EventForm/DateControlsSection/DateTimeSection/TimePicker/TimePicker.tsx", + "EventForm/DateControlsSection/DateTimeSection/TimePicker/TimePickers.tsx", + "EventForm/DateControlsSection/RecurrenceSection/RecurrenceSection.tsx", + "SomedayEventForm/SomedayEventForm.tsx", + "SomedayEventForm/SomedayRecurrenceSection/SomedayRecurrenceSection.tsx", + "SomedayEventForm/SomedayRecurrenceSection/SomedayRecurrenceSelect/SomedayRecurrenceSelect.tsx", +]; + +const removedStyleModules = [ + "ActionsMenu/styled.ts", + "EventForm/styled.ts", + "EventForm/PrioritySection/styled.ts", + "EventForm/DateControlsSection/DateControlsSection/styled.ts", + "EventForm/DateControlsSection/DateTimeSection/styled.ts", + "EventForm/DateControlsSection/DateTimeSection/DatePickers/styled.ts", + "EventForm/DateControlsSection/DateTimeSection/TimePicker/styled.ts", + "EventForm/DateControlsSection/RecurrenceSection/styled.ts", + "SomedayEventForm/styled.ts", + "SomedayEventForm/SomedayRecurrenceSection/SomedayRecurrenceSelect/styled.ts", +]; + +describe("forms Tailwind migration", () => { + test.each( + productionFiles, + )("%s has no styled-components dependency", (file) => { + expect(readFileSync(join(formsRoot, file), "utf8")).not.toMatch( + /styled-components|\/styled["']/, + ); + }); + + test.each(removedStyleModules)("removes %s", (file) => { + expect(existsSync(join(formsRoot, file))).toBe(false); + }); +}); diff --git a/packages/web/src/components/PlannerSidebar/SomedayEventSections/SomedayEvents/SomedayEventContainer/SomedayEventContainer.tsx b/packages/web/src/components/PlannerSidebar/SomedayEventSections/SomedayEvents/SomedayEventContainer/SomedayEventContainer.tsx index 139b181ec..a9959a301 100644 --- a/packages/web/src/components/PlannerSidebar/SomedayEventSections/SomedayEvents/SomedayEventContainer/SomedayEventContainer.tsx +++ b/packages/web/src/components/PlannerSidebar/SomedayEventSections/SomedayEvents/SomedayEventContainer/SomedayEventContainer.tsx @@ -13,8 +13,8 @@ import { useSidebarContext } from "@web/components/PlannerSidebar/draft/context/ import { type Setters_Sidebar } from "@web/components/PlannerSidebar/draft/hooks/useSidebarState"; import { type SomedayInteractionCategory } from "@web/components/PlannerSidebar/SomedayEventSections/interaction/registry/somedayEventRegistry"; import { SomedayEvent } from "@web/components/PlannerSidebar/SomedayEventSections/SomedayEvents/SomedayEvent/SomedayEvent"; +import { FloatingFormContainer } from "@web/views/Forms/SomedayEventForm/FloatingFormContainer"; import { SomedayEventForm } from "@web/views/Forms/SomedayEventForm/SomedayEventForm"; -import { StyledFloatContainer } from "@web/views/Forms/SomedayEventForm/styled"; import { useDraftForm } from "@web/views/Week/components/Draft/hooks/state/useDraftForm"; import { getSidebarOpenWidth } from "@web/views/Week/layout.constants"; @@ -120,7 +120,7 @@ export const SomedayEventContainer = ({ {state.isSomedayFormOpen && isDraftingThisEvent && ( - - + )} diff --git a/packages/web/src/index.css b/packages/web/src/index.css index 0b9d0af55..fbae92be3 100644 --- a/packages/web/src/index.css +++ b/packages/web/src/index.css @@ -51,6 +51,9 @@ --compass-color-text-divider: #5f5f5f; --compass-color-text-gradient-start: hsl(202 100 67); --compass-color-text-gradient-end: hsl(195 78 56); + --compass-radius: 4px; + --compass-text-m: 0.8125rem; + --compass-transition-default: 0.3s; } @theme inline { @@ -660,3 +663,176 @@ @utility overflow-y-auto { @apply compass-scroll; } + +@utility c-actions-menu { + @apply flex flex-col gap-2 rounded-sm p-2; + background-color: var(--actions-menu-bg); + box-shadow: 0 4px 8px rgb(0 0 0 / 10%); +} + +@utility c-actions-menu-item { + @apply flex w-full cursor-pointer items-center gap-2 border-0 px-2 py-1 text-left text-m outline-none; + background-color: var(--actions-menu-item-bg); + color: var(--compass-color-text-dark); + + &:hover, + &:focus-visible { + text-shadow: + 0 0 0.5px var(--compass-color-text-dark), + 0 0 0.5px var(--compass-color-text-dark); + } +} + +@utility c-event-form { + @apply rounded-sm px-5 py-[18px] text-xl transition-[var(--compass-transition-default)]; + background: var(--event-form-bg); + box-shadow: 0 5px 5px var(--compass-color-shadow-default); +} + +@utility c-event-form-title { + @apply bg-transparent text-5xl font-semibold transition-all duration-300; + &:hover { + background-color: var(--compass-color-border-primary); + } +} + +@utility c-event-form-description { + @apply relative max-h-[180px] border-hidden bg-transparent text-xxl font-normal transition-all duration-300; + width: calc(100% - 20px); + &:hover { + background-color: var(--compass-color-border-primary); + filter: brightness(90%); + } +} + +@utility c-time-picker { + @apply relative min-w-[90px] text-lg; + & span[aria-live="polite"], + & .timepicker__indicators { + display: none; + } + & .timepicker__option--is-selected, + & .timepicker__option--is-focused { + background: var(--time-picker-bg); + color: black; + filter: brightness(140%); + } + & .timepicker__control { + min-height: 38px; + border: 0; + border-radius: var(--compass-radius); + background: var(--time-picker-bg); + box-shadow: none; + transition: var(--compass-transition-default); + } + & .timepicker__control:hover { + filter: brightness(90%); + } + & .timepicker__control--is-focused { + box-shadow: 0 0 0 2px var(--compass-color-border-primary-dark); + } + & .timepicker__value-container { + height: 100%; + padding: 0 8px; + } + & .timepicker__single-value { + color: var(--compass-color-text-dark); + } + & .timepicker__menu { + min-width: 150px; + border-radius: 2px; + background: var(--time-picker-bg); + filter: brightness(87%); + } + & .timepicker__menu-list { + padding: 0; + font-size: var(--compass-text-m); + transition: var(--compass-transition-default); + } + & .timepicker__menu-list::-webkit-scrollbar { + width: 8px; + } + & .timepicker__menu-list::-webkit-scrollbar-thumb { + border-radius: var(--compass-radius); + background: var(--time-picker-scrollbar); + } + & .timepicker__menu-list::-webkit-scrollbar-thumb:hover { + background: var(--time-picker-scrollbar-hover); + transition: background-color 0.2s; + } +} + +@utility c-recurrence-row { + @apply mb-1 flex w-full basis-full items-center gap-2 p-0; +} + +@utility c-recurrence-toggle { + @apply inline-flex items-center gap-1 rounded-sm border border-transparent bg-transparent px-2 py-0.5 text-m transition-all duration-300; + color: var(--compass-color-text-dark); + &[data-repeat="false"] { + color: var(--compass-color-text-dark-placeholder); + opacity: 0.85; + } + &[data-disabled="true"] { + cursor: not-allowed; + opacity: 0.7; + } + &:not([data-disabled="true"]):hover { + background: var(--compass-color-border-primary); + } + &:focus-visible { + box-shadow: 0 0 0 2px var(--compass-color-border-primary-dark); + } + & svg { + margin-right: 0.25rem; + color: inherit; + } +} + +@utility c-recurrence-weekday { + @apply size-6 cursor-pointer rounded-full border text-m transition-all duration-300; + border-color: var(--compass-color-border-primary-dark); + background: var(--weekday-bg); + &[data-selected="true"] { + background: var(--weekday-selected-bg); + color: var(--weekday-selected-text); + } + &[data-selected="false"]:hover { + background: var(--compass-color-bg-primary); + color: var(--compass-color-text-light); + } + &:focus { + box-shadow: 0 0 0 2px var(--compass-color-border-primary-dark); + } +} + +@utility c-recurrence-interval { + @apply ml-1 h-[38px] w-8 rounded-sm border border-transparent px-1 text-center text-s transition-all duration-300; + background: var(--recurrence-bg); + &::-webkit-outer-spin-button, + &::-webkit-inner-spin-button { + margin: 0; + appearance: none; + } + &[type="number"] { + appearance: textfield; + } + &:hover { + filter: brightness(90%); + } + &:focus { + box-shadow: 0 0 0 2px var(--compass-color-border-primary-dark); + } +} + +@utility c-recurrence-caret { + @apply flex size-[19px] cursor-pointer items-center justify-center rounded-sm border-0 bg-transparent p-0 transition-[var(--compass-transition-default)]; + color: inherit; + &:hover { + background: var(--compass-color-bg-primary); + color: var(--compass-color-text-light); + } + &:focus { + box-shadow: 0 0 0 2px var(--compass-color-border-primary-dark); + } +} diff --git a/packages/web/src/views/Forms/ActionsMenu/ActionsMenu.tsx b/packages/web/src/views/Forms/ActionsMenu/ActionsMenu.tsx index 006609807..597a4900e 100644 --- a/packages/web/src/views/Forms/ActionsMenu/ActionsMenu.tsx +++ b/packages/web/src/views/Forms/ActionsMenu/ActionsMenu.tsx @@ -29,10 +29,6 @@ import { } from "@web/common/constants/web.constants"; import { useGridMaxZIndex } from "@web/common/hooks/useGridMaxZIndex"; import IconButton from "@web/components/IconButton/IconButton"; -import { - StyledMenu, - TriggerWrapper, -} from "@web/views/Forms/ActionsMenu/styled"; interface MenuContextValue { getItemProps: ( @@ -153,7 +149,8 @@ export const ActionsMenu: React.FC = ({ return ( <> - ) => { @@ -174,7 +171,7 @@ export const ActionsMenu: React.FC = ({ > - +
{open && ( = ({ initialFocus={openedByMouseRef.current ? -1 : 0} returnFocus={false} > - = ({ > {children(closeMenu)} - +
)} diff --git a/packages/web/src/views/Forms/ActionsMenu/MenuItem.tsx b/packages/web/src/views/Forms/ActionsMenu/MenuItem.tsx index b47007c42..bcb71cdea 100644 --- a/packages/web/src/views/Forms/ActionsMenu/MenuItem.tsx +++ b/packages/web/src/views/Forms/ActionsMenu/MenuItem.tsx @@ -7,7 +7,6 @@ import { TooltipTrigger, } from "@web/components/Tooltip"; import { useMenuContext } from "./ActionsMenu"; -import { StyledMenuItem } from "./styled"; export interface MenuItemProps extends React.ButtonHTMLAttributes { @@ -82,17 +81,18 @@ const MenuItem: React.FC = ({ placement="right-end" > - {children} - + {tooltipContent} diff --git a/packages/web/src/views/Forms/ActionsMenu/styled.ts b/packages/web/src/views/Forms/ActionsMenu/styled.ts deleted file mode 100644 index d1f412dca..000000000 --- a/packages/web/src/views/Forms/ActionsMenu/styled.ts +++ /dev/null @@ -1,56 +0,0 @@ -import styled from "styled-components"; -import { ZIndex } from "@web/common/constants/web.constants"; - -export const TriggerWrapper = styled.div` - display: inline-flex; -`; - -interface StyledMenuProps { - bgColor: string; -} - -export const StyledMenu = styled.div` - display: flex; - flex-direction: column; - gap: 8px; - background-color: ${({ bgColor }) => bgColor}; - padding: 8px; - border-radius: ${({ theme }) => theme.shape.borderRadius || "6px"}; - box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1); - z-index: ${ZIndex.LAYER_3}; -`; - -/** - * Shared menu item styling for the EventActionMenu buttons. - */ -interface StyledMenuItemProps { - bgColor: string; -} - -export const StyledMenuItem = styled.button` - display: flex; - align-items: center; - gap: 8px; - width: 100%; - border: none; - padding: 4px 8px; - cursor: pointer; - background-color: ${({ bgColor }) => bgColor}; - color: ${({ theme }) => theme.color.text.dark}; - font-size: ${({ theme }) => theme.text.size.m}; - text-align: left; - - &:hover { - text-shadow: ${({ theme }) => `0 0 0.5px ${theme.color.text.dark}, - 0 0 0.5px ${theme.color.text.dark}`}; - } - - &:focus { - outline: none; - } - - &:focus-visible { - text-shadow: ${({ theme }) => `0 0 0.5px ${theme.color.text.dark}, - 0 0 0.5px ${theme.color.text.dark}`}; - } -`; diff --git a/packages/web/src/views/Forms/EventForm/DateControlsSection/DateControlsSection/DateControlsSection.tsx b/packages/web/src/views/Forms/EventForm/DateControlsSection/DateControlsSection/DateControlsSection.tsx index c3b338fe4..7c5f8a900 100644 --- a/packages/web/src/views/Forms/EventForm/DateControlsSection/DateControlsSection/DateControlsSection.tsx +++ b/packages/web/src/views/Forms/EventForm/DateControlsSection/DateControlsSection/DateControlsSection.tsx @@ -3,7 +3,6 @@ import { DateTimeSection, type Props as DateTimeSectionProps, } from "../DateTimeSection/DateTimeSection"; -import { StyledControlsSection } from "./styled"; interface Props { dateTimeSectionProps: DateTimeSectionProps; @@ -12,8 +11,8 @@ interface Props { export const DateControlsSection = ({ dateTimeSectionProps }: Props) => { return ( - +
- +
); }; diff --git a/packages/web/src/views/Forms/EventForm/DateControlsSection/DateControlsSection/styled.ts b/packages/web/src/views/Forms/EventForm/DateControlsSection/DateControlsSection/styled.ts deleted file mode 100644 index ef8869ab1..000000000 --- a/packages/web/src/views/Forms/EventForm/DateControlsSection/DateControlsSection/styled.ts +++ /dev/null @@ -1,9 +0,0 @@ -import styled from "styled-components"; - -export const StyledControlsSection = styled.div` - margin-top: 15px; - margin-bottom: 12px; - display: flex; - flex-wrap: wrap; - gap: ${({ theme }) => theme.spacing.s}; -`; diff --git a/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/DatePickers/DatePickers.tsx b/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/DatePickers/DatePickers.tsx index a364cbe24..0d33dad9f 100644 --- a/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/DatePickers/DatePickers.tsx +++ b/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/DatePickers/DatePickers.tsx @@ -6,8 +6,7 @@ import dayjs from "@core/util/date/dayjs"; import { dateIsValid } from "@web/common/utils/datetime/web.date.util"; import { shouldAdjustComplimentDate } from "@web/common/utils/datetime/web.datetime.util"; import { DatePicker } from "@web/components/DatePicker/DatePicker"; -import { AlignItems } from "@web/components/Flex/Flex"; -import { StyledDateFlex } from "@web/views/Forms/EventForm/DateControlsSection/DateTimeSection/DatePickers/styled"; +import { AlignItems, Flex } from "@web/components/Flex/Flex"; import { type SetEventFormField } from "@web/views/Forms/EventForm/types"; const stopPropagation = (e: React.MouseEvent) => { @@ -178,7 +177,7 @@ export const DatePickers: FC = ({ return ( <> - + {/* biome-ignore lint/a11y/noStaticElementInteractions: This wrapper only stops date picker mouse events from bubbling to the form. */}
= ({ view="grid" />
-
+ - + {/* biome-ignore lint/a11y/noStaticElementInteractions: This wrapper only stops date picker mouse events from bubbling to the form. */}
= ({ view="grid" />
-
+ ); }; diff --git a/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/DatePickers/styled.ts b/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/DatePickers/styled.ts deleted file mode 100644 index 98e115da5..000000000 --- a/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/DatePickers/styled.ts +++ /dev/null @@ -1,6 +0,0 @@ -import styled from "styled-components"; -import { Flex } from "@web/components/Flex/Flex"; - -export const StyledDateFlex = styled(Flex)` - width: 120px; -`; diff --git a/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/DateTimeSection.tsx b/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/DateTimeSection.tsx index ef897e417..ca3c003de 100644 --- a/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/DateTimeSection.tsx +++ b/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/DateTimeSection.tsx @@ -1,9 +1,8 @@ import { type FC, type SetStateAction } from "react"; import { Categories_Event, type Schema_Event } from "@core/types/event.types"; import { type SelectOption } from "@web/common/types/component.types"; -import { AlignItems } from "@web/components/Flex/Flex"; +import { AlignItems, Flex } from "@web/components/Flex/Flex"; import { DatePickers } from "@web/views/Forms/EventForm/DateControlsSection/DateTimeSection/DatePickers/DatePickers"; -import { StyledDateTimeFlex } from "@web/views/Forms/EventForm/DateControlsSection/DateTimeSection/styled"; import { TimePickers } from "@web/views/Forms/EventForm/DateControlsSection/DateTimeSection/TimePicker/TimePickers"; import { type SetEventFormField } from "@web/views/Forms/EventForm/types"; @@ -53,7 +52,7 @@ export const DateTimeSection: FC = ({ endTime, }) => { return ( - + {category === Categories_Event.ALLDAY && ( = ({ selectedStartDate={selectedStartDate} /> )} - + ); }; diff --git a/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/TimePicker/TimePicker.tsx b/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/TimePicker/TimePicker.tsx index 9c4bb91a5..4284d366b 100644 --- a/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/TimePicker/TimePicker.tsx +++ b/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/TimePicker/TimePicker.tsx @@ -1,8 +1,9 @@ import type React from "react"; import ReactSelect, { type Props as RSProps } from "react-select"; +import { darken } from "@core/util/color.utils"; +import { type CSSVariables } from "@web/common/styles/css.types"; import { type SelectOption } from "@web/common/types/component.types"; import { type Option_Time } from "@web/common/types/util.types"; -import { StyledTimePicker } from "./styled"; export interface Props extends Omit { bgColor: string; @@ -28,7 +29,16 @@ export const TimePicker = ({ let scrollTimer: number; return ( - +
- +
); }; diff --git a/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/TimePicker/TimePickers.tsx b/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/TimePicker/TimePickers.tsx index 17d167c62..e740c43b6 100644 --- a/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/TimePicker/TimePickers.tsx +++ b/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/TimePicker/TimePickers.tsx @@ -8,8 +8,7 @@ import { mapToBackend, } from "@web/common/utils/datetime/web.date.util"; import { shouldAdjustComplimentTime } from "@web/common/utils/datetime/web.datetime.util"; -import { AlignItems } from "@web/components/Flex/Flex"; -import { StyledTimeFlex } from "../styled"; +import { AlignItems, Flex } from "@web/components/Flex/Flex"; import { TimePicker } from "./TimePicker"; interface Props { @@ -125,7 +124,7 @@ export const TimePickers: FC = ({ }; return ( - + = ({ setIsMenuOpen={setIsEndMenuOpen} value={endTime} /> - + ); }; diff --git a/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/TimePicker/styled.ts b/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/TimePicker/styled.ts deleted file mode 100644 index 681a49d1c..000000000 --- a/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/TimePicker/styled.ts +++ /dev/null @@ -1,86 +0,0 @@ -import styled from "styled-components"; -import { darken } from "@core/util/color.utils"; -import { Divider } from "@web/components/Divider/Divider"; - -export interface Props { - bgColor?: string; -} - -export const StyledTimePicker = styled.div` - font-size: ${({ theme }) => theme.text.size.l}; - min-width: 90px; - position: relative; - - & span[aria-live="polite"] { - display: none; - } - - & .timepicker { - &__option { - &--is-selected, - &--is-focused { - background-color: ${({ bgColor }) => bgColor}; - color: black; - filter: brightness(140%); - } - } - - &__control { - border: none; - ${({ bgColor }) => bgColor && `background: ${bgColor}`}; - border-radius: ${({ theme }) => theme.shape.borderRadius}; - box-shadow: none; - min-height: 38px; - transition: ${({ theme }) => theme.transition.default}; - &:hover { - filter: brightness(90%); - } - &--is-focused { - box-shadow: 0 0 0 2px ${({ theme }) => theme.color.border.primaryDark}; - } - } - &__value-container { - height: 100%; - padding: 0px 8px; - } - - &__indicators { - display: none; - } - - &__single-value { - color: ${({ theme }) => theme.color.text.dark}; - } - - &__menu { - background: ${({ bgColor }) => bgColor}; - filter: brightness(87%); - border-radius: 2px; - min-width: 150px; - - &-list { - font-size: ${({ theme }) => theme.text.size.m}; - padding: 0; - transition: ${({ theme }) => theme.transition.default}; - - ::-webkit-scrollbar { - width: 8px; - } - ::-webkit-scrollbar-thumb { - background: ${({ bgColor }) => bgColor && darken(bgColor, 40)}; - border-radius: ${({ theme }) => theme.shape.borderRadius}; - &:hover { - background: ${({ bgColor }) => bgColor && darken(bgColor, 80)}; - transition: background-color 0.2s; - } - } - } - } - } -`; - -export const StyledDivider = styled(Divider)` - position: absolute; - bottom: 2px; - left: 2px; -`; diff --git a/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/styled.ts b/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/styled.ts deleted file mode 100644 index fd2310d14..000000000 --- a/packages/web/src/views/Forms/EventForm/DateControlsSection/DateTimeSection/styled.ts +++ /dev/null @@ -1,8 +0,0 @@ -import styled from "styled-components"; -import { Flex } from "@web/components/Flex/Flex"; - -export const StyledDateTimeFlex = styled(Flex)` - gap: ${({ theme }) => theme.spacing.s}; -`; - -export const StyledTimeFlex = styled(Flex)``; diff --git a/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/RecurrenceSection.tsx b/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/RecurrenceSection.tsx index 3f517bc29..34c0894cf 100644 --- a/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/RecurrenceSection.tsx +++ b/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/RecurrenceSection.tsx @@ -6,12 +6,10 @@ import { useSession } from "@web/auth/compass/session/useSession"; import { isBackendUnavailable as getIsBackendUnavailable } from "@web/common/apis/util/backend-unavailable-error.util"; import { hoverColorByPriority } from "@web/common/styles/theme.util"; import { ConditionalRender } from "@web/components/ConditionalRender/ConditionalRender"; -import { FlexDirections } from "@web/components/Flex/Flex"; import { EndsOnDate } from "@web/views/Forms/EventForm/DateControlsSection/RecurrenceSection/components/EndsOnDate"; import { RecurrenceIntervalSelect } from "@web/views/Forms/EventForm/DateControlsSection/RecurrenceSection/components/RecurrenceIntervalSelect"; import { RecurrenceToggle } from "@web/views/Forms/EventForm/DateControlsSection/RecurrenceSection/components/RecurrenceToggle"; import { WeekDays } from "@web/views/Forms/EventForm/DateControlsSection/RecurrenceSection/components/WeekDays"; -import { StyledRepeatRow } from "@web/views/Forms/EventForm/DateControlsSection/RecurrenceSection/styled"; import { useRecurrence } from "@web/views/Forms/EventForm/DateControlsSection/RecurrenceSection/useRecurrence/useRecurrence"; export interface RecurrenceSectionProps { @@ -45,7 +43,7 @@ export function createRecurrenceSection({ const disabledMessage = "Sign in to use recurring events."; return ( - +
- +
); }; } diff --git a/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/components/CaretInput.tsx b/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/components/CaretInput.tsx index 063966fc8..cbe261aa5 100644 --- a/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/components/CaretInput.tsx +++ b/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/components/CaretInput.tsx @@ -1,9 +1,5 @@ import { CaretDown, CaretUp } from "@phosphor-icons/react"; import type React from "react"; -import { - StyledCaretButton, - StyledCaretInputContainer, -} from "@web/views/Forms/EventForm/DateControlsSection/RecurrenceSection/styled"; export interface CaretInputProps { onChange: (type: "increase" | "decrease") => void; @@ -11,8 +7,10 @@ export interface CaretInputProps { export const CaretInput = ({ onChange }: CaretInputProps) => { return ( - - + - ) => { event.preventDefault(); event.stopPropagation(); @@ -30,7 +30,7 @@ export const CaretInput = ({ onChange }: CaretInputProps) => { }} > - - + +
); }; diff --git a/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/components/EndsOnDate.tsx b/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/components/EndsOnDate.tsx index 4d9b844e3..905c05c4e 100644 --- a/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/components/EndsOnDate.tsx +++ b/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/components/EndsOnDate.tsx @@ -7,7 +7,6 @@ import { DatePicker } from "@web/components/DatePicker/DatePicker"; import { Flex } from "@web/components/Flex/Flex"; import { Text } from "@web/components/Text/Text"; import { TooltipWrapper } from "@web/components/Tooltip/TooltipWrapper"; -import { StyledRepeatRow } from "@web/views/Forms/EventForm/DateControlsSection/RecurrenceSection/styled"; export interface EndsOnDateProps { bgColor: string; @@ -28,7 +27,7 @@ export const EndsOnDate = ({ const miniDate = useMemo(() => parseCompassEventDate(minDate), [minDate]); return ( - +
Ends on: - +
); }; diff --git a/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/components/RecurrenceIntervalSelect.tsx b/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/components/RecurrenceIntervalSelect.tsx index b4c8f4224..709f33e83 100644 --- a/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/components/RecurrenceIntervalSelect.tsx +++ b/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/components/RecurrenceIntervalSelect.tsx @@ -1,10 +1,7 @@ import { useState } from "react"; +import { type CSSVariables } from "@web/common/styles/css.types"; import { Text } from "@web/components/Text/Text"; import { type FrequencyValues } from "@web/views/Forms/EventForm/DateControlsSection/RecurrenceSection/constants/recurrence.constants"; -import { - StyledIntervalInput, - StyledRepeatRow, -} from "@web/views/Forms/EventForm/DateControlsSection/RecurrenceSection/styled"; import { CaretInput } from "./CaretInput"; import { FreqSelect } from "./FreqSelect"; @@ -42,11 +39,12 @@ export const RecurrenceIntervalSelect = ({ }; return ( - +
Every - 1} onFreqSelect={onFreqSelect} /> - +
); }; diff --git a/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/components/RecurrenceToggle.tsx b/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/components/RecurrenceToggle.tsx index 8cc2c5b11..021c27e71 100644 --- a/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/components/RecurrenceToggle.tsx +++ b/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/components/RecurrenceToggle.tsx @@ -1,10 +1,6 @@ import { useCallback } from "react"; import { RepeatIcon } from "@web/components/Icons/Repeat"; import { TooltipWrapper } from "@web/components/Tooltip/TooltipWrapper"; -import { - StyledRepeatButton, - StyledRepeatRow, -} from "@web/views/Forms/EventForm/DateControlsSection/RecurrenceSection/styled"; export interface RecurrenceToggleProps { disabled?: boolean; @@ -25,26 +21,27 @@ export const RecurrenceToggle = ({ }, [disabled, toggleRecurrence]); const toggle = ( - Repeat - + ); return ( - +
{disabled && disabledMessage ? ( {toggle} ) : ( toggle )} - +
); }; diff --git a/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/components/WeekDay.tsx b/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/components/WeekDay.tsx index 53a24005f..33a1922c9 100644 --- a/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/components/WeekDay.tsx +++ b/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/components/WeekDay.tsx @@ -1,5 +1,7 @@ import type React from "react"; -import { StyledWeekDay } from "@web/views/Forms/EventForm/DateControlsSection/RecurrenceSection/styled"; +import { darken } from "@core/util/color.utils"; +import { type CSSVariables } from "@web/common/styles/css.types"; +import { theme } from "@web/common/styles/theme"; export interface WeekDayProps { day: string; @@ -10,16 +12,24 @@ export interface WeekDayProps { export const WeekDay = ({ day, bgColor, onClick, selected }: WeekDayProps) => { return ( - ) => { event.preventDefault(); event.stopPropagation(); onClick(); }} - selected={selected} > {day.charAt(0).toUpperCase()} - + ); }; diff --git a/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/components/WeekDays.tsx b/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/components/WeekDays.tsx index 766c00524..0cd24913f 100644 --- a/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/components/WeekDays.tsx +++ b/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/components/WeekDays.tsx @@ -1,7 +1,6 @@ import type React from "react"; import { Text } from "@web/components/Text/Text"; import { WEEKDAYS } from "@web/views/Forms/EventForm/DateControlsSection/RecurrenceSection/constants/recurrence.constants"; -import { StyledRepeatRow } from "@web/views/Forms/EventForm/DateControlsSection/RecurrenceSection/styled"; import { WeekDay } from "./WeekDay"; export interface WeekDaysProps { @@ -16,7 +15,7 @@ export const WeekDays: React.FC = ({ onChange, }) => { return ( - +
On: {WEEKDAYS.map((day) => ( @@ -34,6 +33,6 @@ export const WeekDays: React.FC = ({ selected={value.includes(day)} /> ))} - +
); }; diff --git a/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/styled.ts b/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/styled.ts deleted file mode 100644 index c4653df20..000000000 --- a/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/styled.ts +++ /dev/null @@ -1,151 +0,0 @@ -import styled from "styled-components"; -import { darken } from "@core/util/color.utils"; -import { Flex } from "@web/components/Flex/Flex"; - -export const StyledRepeatRow = styled(Flex)` - align-items: center; - flex-basis: 100%; - gap: ${({ theme }) => theme.spacing.s}; - margin-bottom: ${({ theme }) => theme.spacing.xs}; - padding: 0; - width: 100%; -`; - -export const StyledRepeatButton = styled.button<{ - $disabled?: boolean; - hasRepeat: boolean; -}>` - display: inline-flex; - align-items: center; - background: transparent; - gap: ${({ theme }) => theme.spacing.xs}; - border: 1px solid transparent; - border-radius: ${({ theme }) => theme.shape.borderRadius}; - font: inherit; - font-size: ${({ theme }) => theme.text.size.m}; - margin-bottom: ${({ hasRepeat, theme }) => - hasRepeat ? theme.spacing.xs : 0}; - opacity: ${({ $disabled, hasRepeat }) => - $disabled ? 0.7 : !hasRepeat ? 0.85 : 1}; - padding: 2px 8px; - color: ${({ hasRepeat, theme }) => - hasRepeat ? theme.color.text.dark : theme.color.text.darkPlaceholder}; - cursor: ${({ $disabled }) => ($disabled ? "not-allowed" : "pointer")}; - transition: ${({ theme }) => theme.transition.default}; - - svg { - color: inherit; - margin-right: ${({ theme }) => theme.spacing.xs}; - } - - &:hover { - color: ${({ $disabled, theme }) => - $disabled ? theme.color.text.darkPlaceholder : theme.color.text.dark}; - background-color: ${({ $disabled, theme }) => - $disabled ? "transparent" : theme.color.border.primary}; - } - &:focus-visible { - box-shadow: 0 0 0 2px ${({ theme }) => theme.color.border.primaryDark}; - } -`; - -export const StyledWeekDay = styled.button<{ - bgColor: string; - selected: boolean; -}>` - background-color: ${({ bgColor }) => bgColor}; - border: 1px solid ${({ theme }) => theme.color.border.primaryDark}; - border-radius: 50%; - font-size: ${({ theme }) => theme.text.size.m}; - height: 24px; - transition: ${({ theme }) => theme.transition.default}; - width: 24px; - - cursor: pointer; - ${({ selected, theme }) => - !selected && - ` - &:hover { - background-color: ${theme.color.bg.primary}; - color: ${theme.color.text.light}; - } - `} - - ${({ selected, bgColor, theme }) => - selected && - ` - background-color: ${darken(bgColor, 30)}; - color: ${theme.getContrastText(bgColor)}; - `} - - &:focus { - box-shadow: 0 0 0 2px ${({ theme }) => theme.color.border.primaryDark}; - } -`; - -export const StyledIntervalInput = styled.input<{ - bgColor: string; -}>` - width: 32px; - height: 38px; - border: 1px solid transparent; - text-align: center; - border-radius: ${({ theme }) => theme.shape.borderRadius}; - font-size: ${({ theme }) => theme.text.size.s}; - padding: 0 4px; - background-color: ${({ bgColor }) => bgColor}; - margin-left: ${({ theme }) => theme.spacing.xs}; - transition: ${({ theme }) => theme.transition.default}; - - /* Hide arrows/spinners */ - &::-webkit-outer-spin-button, - &::-webkit-inner-spin-button { - -webkit-appearance: none; - margin: 0; - } - - /* Firefox */ - &[type="number"] { - -moz-appearance: textfield; - } - - &:hover { - filter: brightness(90%); - } - - &:focus { - box-shadow: 0 0 0 2px ${({ theme }) => theme.color.border.primaryDark}; - } -`; - -export const StyledCaretInputContainer = styled.div` - display: flex; - flex-direction: column; - margin-left: ${({ theme }) => theme.spacing.xs}; - justify-content: space-between; -`; - -export const StyledCaretButton = styled.button` - background: none; - color: inherit; - border: none; - border-radius: ${({ theme }) => theme.shape.borderRadius}; - padding: 0; - font: inherit; - cursor: pointer; - outline: inherit; - height: 19px; - width: 19px; - display: flex; - align-items: center; - justify-content: center; - transition: ${({ theme }) => theme.transition.default}; - - &:hover { - background-color: ${({ theme }) => theme.color.bg.primary}; - color: ${({ theme }) => theme.color.text.light}; - } - &:focus { - box-shadow: 0 0 0 2px ${({ theme }) => theme.color.border.primaryDark}; - } -`; diff --git a/packages/web/src/views/Forms/EventForm/EventForm.tsx b/packages/web/src/views/Forms/EventForm/EventForm.tsx index fecad3f60..5091abd82 100644 --- a/packages/web/src/views/Forms/EventForm/EventForm.tsx +++ b/packages/web/src/views/Forms/EventForm/EventForm.tsx @@ -14,6 +14,7 @@ import { darken } from "@core/util/color.utils"; import dayjs from "@core/util/date/dayjs"; import { ID_EVENT_FORM } from "@web/common/constants/web.constants"; import { useAppHotkey } from "@web/common/hooks/useAppHotkey"; +import { type CSSVariables } from "@web/common/styles/css.types"; import { colorByPriority, hoverColorByPriority, @@ -22,18 +23,15 @@ import { type SelectOption } from "@web/common/types/component.types"; import { mapToBackend } from "@web/common/utils/datetime/web.date.util"; import { getCategory } from "@web/common/utils/event/event.util"; import { isComboboxInteraction } from "@web/common/utils/form/form.util"; +import { Flex } from "@web/components/Flex/Flex"; +import { Input } from "@web/components/Input/Input"; +import { Textarea } from "@web/components/Textarea/Textarea"; import { DateControlsSection } from "@web/views/Forms/EventForm/DateControlsSection/DateControlsSection/DateControlsSection"; import { getFormDates } from "@web/views/Forms/EventForm/DateControlsSection/DateTimeSection/form.datetime.util"; import { RecurrenceSection } from "@web/views/Forms/EventForm/DateControlsSection/RecurrenceSection/RecurrenceSection"; import { EventActionMenu } from "@web/views/Forms/EventForm/EventActionMenu"; import { PrioritySection } from "@web/views/Forms/EventForm/PrioritySection"; import { SaveSection } from "@web/views/Forms/EventForm/SaveSection"; -import { - StyledDescription, - StyledEventForm, - StyledIconRow, - StyledTitle, -} from "@web/views/Forms/EventForm/styled"; import { type FormProps, type SetEventFormField, @@ -455,8 +453,9 @@ export const EventForm: React.FC> = memo( ); return ( - { if (isStartDatePickerOpen) { @@ -470,10 +469,11 @@ export const EventForm: React.FC> = memo( onMouseDown={(e) => { e.stopPropagation(); }} - priority={priority} - role="form" + style={ + { "--event-form-bg": hoverColorByPriority[priority] } as CSSVariables + } > - + > = memo( onDuplicate={onDuplicateEvent} onDelete={onDelete} /> - + - > = memo( - - + ); }, fastDeepEqual, diff --git a/packages/web/src/views/Forms/EventForm/MoveToSidebarMenuButton.tsx b/packages/web/src/views/Forms/EventForm/MoveToSidebarMenuButton.tsx index ac4d610ad..3bbeb08fe 100644 --- a/packages/web/src/views/Forms/EventForm/MoveToSidebarMenuButton.tsx +++ b/packages/web/src/views/Forms/EventForm/MoveToSidebarMenuButton.tsx @@ -1,6 +1,5 @@ import { ArrowLeftIcon } from "@phosphor-icons/react"; import type React from "react"; -import styled from "styled-components"; import { getMetaKeyIcon } from "@web/common/utils/shortcut/shortcut.util"; import { Text } from "@web/components/Text/Text"; import MenuItem from "@web/views/Forms/ActionsMenu/MenuItem"; @@ -11,11 +10,6 @@ interface Props { bgColor: string; } -const StyledArrowLeft = styled(ArrowLeftIcon)` - width: 14px; - height: 14px; -`; - export const MoveToSidebarMenuButton: React.FC = ({ onClick, label = "Move To Sidebar", @@ -28,7 +22,7 @@ export const MoveToSidebarMenuButton: React.FC = ({ aria-label={label} tooltipContent={ - CTRL + {getMetaKeyIcon({ size: 14 })} + + CTRL + {getMetaKeyIcon({ size: 14 })} + } > diff --git a/packages/web/src/views/Forms/EventForm/PrioritySection/PrioritySection.tsx b/packages/web/src/views/Forms/EventForm/PrioritySection/PrioritySection.tsx index d4cd51a34..0035572b5 100644 --- a/packages/web/src/views/Forms/EventForm/PrioritySection/PrioritySection.tsx +++ b/packages/web/src/views/Forms/EventForm/PrioritySection/PrioritySection.tsx @@ -2,8 +2,8 @@ import type React from "react"; import { Priorities, type Priority } from "@core/constants/core.constants"; import { type CSSVariables } from "@web/common/styles/css.types"; import { colorByPriority } from "@web/common/styles/theme.util"; +import { Flex } from "@web/components/Flex/Flex"; import { type SetEventFormField } from "../types"; -import { StyledPriorityFlex } from "./styled"; interface Props { priority: Priority; @@ -25,7 +25,7 @@ export const PrioritySection: React.FC = ({ ]; return ( - + {priorities.map((item) => (
= ({ {item.label}
))} -
+ ); }; diff --git a/packages/web/src/views/Forms/EventForm/PrioritySection/styled.ts b/packages/web/src/views/Forms/EventForm/PrioritySection/styled.ts deleted file mode 100644 index 348030086..000000000 --- a/packages/web/src/views/Forms/EventForm/PrioritySection/styled.ts +++ /dev/null @@ -1,7 +0,0 @@ -import styled from "styled-components"; -import { Flex } from "@web/components/Flex/Flex"; - -export const StyledPriorityFlex = styled(Flex)` - margin: 15px 0; - gap: 10px; -`; diff --git a/packages/web/src/views/Forms/EventForm/SaveSection/SaveSection.tsx b/packages/web/src/views/Forms/EventForm/SaveSection/SaveSection.tsx index 24ee0c67b..2c54aa78f 100644 --- a/packages/web/src/views/Forms/EventForm/SaveSection/SaveSection.tsx +++ b/packages/web/src/views/Forms/EventForm/SaveSection/SaveSection.tsx @@ -3,9 +3,9 @@ import { useCallback } from "react"; import { type Priority } from "@core/constants/core.constants"; import { getModifierKeyIcon } from "@web/common/utils/shortcut/shortcut.util"; import { Btn, SaveButton } from "@web/components/Button/Button"; +import { Flex } from "@web/components/Flex/Flex"; import { Text } from "@web/components/Text/Text"; import { TooltipWrapper } from "@web/components/Tooltip/TooltipWrapper"; -import { StyledSubmitRow } from "@web/views/Forms/EventForm/styled"; interface Props { saveText?: string; @@ -25,7 +25,7 @@ export const SaveSection: React.FC = ({ const onSave = useCallback(() => _onSubmit(), [_onSubmit]); return ( - + {onCancel && ( = ({ {saveText} - + ); }; diff --git a/packages/web/src/views/Forms/EventForm/styled.ts b/packages/web/src/views/Forms/EventForm/styled.ts deleted file mode 100644 index 46ca79908..000000000 --- a/packages/web/src/views/Forms/EventForm/styled.ts +++ /dev/null @@ -1,71 +0,0 @@ -import styled from "styled-components"; -import { ZIndex } from "@web/common/constants/web.constants"; -import { hoverColorByPriority } from "@web/common/styles/theme.util"; -import { PriorityButton } from "@web/components/Button/Button"; -import { Flex } from "@web/components/Flex/Flex"; -import { Input } from "@web/components/Input/Input"; -import { Textarea } from "@web/components/Textarea/Textarea"; -import { EVENT_WIDTH_MINIMUM } from "@web/views/Week/layout.constants"; -import { type StyledFormProps } from "./types"; - -interface SomedayFormProps extends StyledFormProps { - x?: number; - y?: number; -} - -export const StyledEventForm = styled.form` - background: ${({ priority }) => - priority ? hoverColorByPriority[priority] : undefined}; - border-radius: ${({ theme }) => theme.shape.borderRadius}; - box-shadow: 0px 5px 5px ${({ theme }) => theme.color.shadow.default}; - font-size: 20px; - padding: 18px 20px; - transition: ${({ theme }) => theme.transition.default}; - z-index: ${ZIndex.LAYER_1}; -`; - -export const StyledDescription = styled(Textarea)` - background: transparent; - border: hidden; - font-size: ${({ theme }) => theme.text.size.xxl}; - font-weight: ${({ theme }) => theme.text.weight.regular}; - max-height: 180px; - position: relative; - width: calc(100% - 20px) !important; - transition: ${({ theme }) => theme.transition.default}; - - &:hover { - filter: brightness(90%); - background-color: ${({ theme }) => theme.color.border.primary}; - } -`; - -export const StyledIconRow = styled(Flex)` - align-items: center; - gap: 30px; - justify-content: end; - margin-bottom: 10px; -`; - -export const StyledSubmitButton = styled(PriorityButton)` - border: 2px solid; - margin-top: 15px; - min-width: ${EVENT_WIDTH_MINIMUM}px; -`; - -export const StyledSubmitRow = styled(Flex)` - align-items: right; - justify-content: end; - padding-top: 18px; -`; - -export const StyledTitle = styled(Input)` - background: transparent; - font-size: ${({ theme }) => theme.text.size["5xl"]}; - font-weight: 600; - transition: ${({ theme }) => theme.transition.default}; - - &:hover { - background-color: ${({ theme }) => theme.color.border.primary}; - } -`; diff --git a/packages/web/src/views/Forms/EventForm/types.ts b/packages/web/src/views/Forms/EventForm/types.ts index 7417d3da6..9177479d4 100644 --- a/packages/web/src/views/Forms/EventForm/types.ts +++ b/packages/web/src/views/Forms/EventForm/types.ts @@ -40,8 +40,3 @@ export type SetEventFormField = ( field: Partial, value?: Schema_Event[EventField], ) => void; - -export interface StyledFormProps { - priority?: Priority; - title?: string; -} diff --git a/packages/web/src/views/Forms/SomedayEventForm/FloatingFormContainer.tsx b/packages/web/src/views/Forms/SomedayEventForm/FloatingFormContainer.tsx new file mode 100644 index 000000000..e4fa21306 --- /dev/null +++ b/packages/web/src/views/Forms/SomedayEventForm/FloatingFormContainer.tsx @@ -0,0 +1,28 @@ +import { forwardRef, type HTMLAttributes } from "react"; +import { Z_INDEX_FLOATING_FORM } from "@web/common/constants/web.constants"; + +interface FloatingFormContainerProps extends HTMLAttributes { + strategy: "fixed" | "absolute"; + left: number; + top: number; +} + +export const FloatingFormContainer = forwardRef< + HTMLDivElement, + FloatingFormContainerProps +>(({ strategy, left, top, style, ...props }, ref) => ( +
+)); + +FloatingFormContainer.displayName = "FloatingFormContainer"; diff --git a/packages/web/src/views/Forms/SomedayEventForm/SomedayEventForm.tsx b/packages/web/src/views/Forms/SomedayEventForm/SomedayEventForm.tsx index ed840bc81..3a9b0d7f8 100644 --- a/packages/web/src/views/Forms/SomedayEventForm/SomedayEventForm.tsx +++ b/packages/web/src/views/Forms/SomedayEventForm/SomedayEventForm.tsx @@ -9,16 +9,17 @@ import { Priorities } from "@core/constants/core.constants"; import { Categories_Event } from "@core/types/event.types"; import { darken } from "@core/util/color.utils"; import { ID_SOMEDAY_EVENT_FORM } from "@web/common/constants/web.constants"; -import { colorByPriority } from "@web/common/styles/theme.util"; +import { type CSSVariables } from "@web/common/styles/css.types"; +import { + colorByPriority, + hoverColorByPriority, +} from "@web/common/styles/theme.util"; import { isComboboxInteraction } from "@web/common/utils/form/form.util"; +import { Flex } from "@web/components/Flex/Flex"; +import { Input } from "@web/components/Input/Input"; +import { Textarea } from "@web/components/Textarea/Textarea"; import { PrioritySection } from "@web/views/Forms/EventForm/PrioritySection"; import { SaveSection } from "@web/views/Forms/EventForm/SaveSection"; -import { - StyledDescription, - StyledEventForm, - StyledIconRow, - StyledTitle, -} from "@web/views/Forms/EventForm/styled"; import { type FormProps, type SetEventFormField, @@ -158,8 +159,9 @@ export const SomedayEventForm: React.FC = ({ }; return ( - = ({ onMouseUp={(e) => { e.stopPropagation(); }} - priority={priority} - role="form" + style={ + { "--event-form-bg": hoverColorByPriority[priority] } as CSSVariables + } > - + = ({ onDuplicateClick={onDuplicateEvent} onDeleteClick={onDelete} /> - + - = ({ setEvent={setLatestEvent} /> - - + ); }; diff --git a/packages/web/src/views/Forms/SomedayEventForm/SomedayRecurrenceSection/SomedayRecurrenceSection.tsx b/packages/web/src/views/Forms/SomedayEventForm/SomedayRecurrenceSection/SomedayRecurrenceSection.tsx index 7679a3107..0d071da0c 100644 --- a/packages/web/src/views/Forms/SomedayEventForm/SomedayRecurrenceSection/SomedayRecurrenceSection.tsx +++ b/packages/web/src/views/Forms/SomedayEventForm/SomedayRecurrenceSection/SomedayRecurrenceSection.tsx @@ -2,10 +2,6 @@ import React, { type Dispatch, type SetStateAction, useRef } from "react"; import { Frequency } from "rrule"; import { type Schema_Event } from "@core/types/event.types"; import { RepeatIcon } from "@web/components/Icons/Repeat"; -import { - StyledRepeatButton, - StyledRepeatRow, -} from "@web/views/Forms/EventForm/DateControlsSection/RecurrenceSection/styled"; import { useRecurrence } from "../../EventForm/DateControlsSection/RecurrenceSection/useRecurrence/useRecurrence"; import { type SomedayFrequencyOption, @@ -82,7 +78,7 @@ export const SomedayRecurrenceSection = ({ const shouldShowSelect = hasRecurrence || isEditing; return ( - +
{shouldShowSelect ? ( ) : ( - Repeat - + )} - +
); }; diff --git a/packages/web/src/views/Forms/SomedayEventForm/SomedayRecurrenceSection/SomedayRecurrenceSelect/SomedayRecurrenceSelect.tsx b/packages/web/src/views/Forms/SomedayEventForm/SomedayRecurrenceSection/SomedayRecurrenceSelect/SomedayRecurrenceSelect.tsx index d1a2c5bd8..b467fa435 100644 --- a/packages/web/src/views/Forms/SomedayEventForm/SomedayRecurrenceSection/SomedayRecurrenceSelect/SomedayRecurrenceSelect.tsx +++ b/packages/web/src/views/Forms/SomedayEventForm/SomedayRecurrenceSection/SomedayRecurrenceSelect/SomedayRecurrenceSelect.tsx @@ -6,12 +6,10 @@ import ReactSelect, { type SingleValueProps, } from "react-select"; import { Frequency } from "rrule"; -import { useTheme } from "styled-components"; import { brighten, darken } from "@core/util/color.utils"; import { theme } from "@web/common/styles/theme"; import { RepeatIcon } from "@web/components/Icons/Repeat"; import { type FrequencyValues } from "../../../EventForm/DateControlsSection/RecurrenceSection/constants/recurrence.constants"; -import { SelectContent } from "./styled"; export type SomedayFrequencyOption = { label: string; @@ -57,7 +55,7 @@ export const SomedayRecurrenceSelect = ({ : DO_NOT_REPEAT_OPTION, [hasRecurrence, freq], ); - const selectTheme = useTheme(); + const selectTheme = theme; const selectRef = useRef>(null); useEffect(() => { @@ -110,10 +108,10 @@ export const SomedayRecurrenceSelect = ({ return ( - - + ); }; diff --git a/packages/web/src/views/Forms/SomedayEventForm/SomedayRecurrenceSection/SomedayRecurrenceSelect/styled.ts b/packages/web/src/views/Forms/SomedayEventForm/SomedayRecurrenceSection/SomedayRecurrenceSelect/styled.ts deleted file mode 100644 index 1915d0ddb..000000000 --- a/packages/web/src/views/Forms/SomedayEventForm/SomedayRecurrenceSection/SomedayRecurrenceSelect/styled.ts +++ /dev/null @@ -1,10 +0,0 @@ -import styled from "styled-components"; - -export const SelectContent = styled.span<{ dimmed?: boolean }>` - align-items: center; - color: ${({ dimmed, theme }) => - dimmed ? theme.color.text.darkPlaceholder : theme.color.text.dark}; - display: inline-flex; - gap: ${({ theme }) => theme.spacing.xs}; - font-size: ${({ theme }) => theme.text.size.m}; -`; diff --git a/packages/web/src/views/Forms/SomedayEventForm/styled.ts b/packages/web/src/views/Forms/SomedayEventForm/styled.ts deleted file mode 100644 index 2136a1407..000000000 --- a/packages/web/src/views/Forms/SomedayEventForm/styled.ts +++ /dev/null @@ -1,16 +0,0 @@ -import styled from "styled-components"; -import { Z_INDEX_FLOATING_FORM } from "@web/common/constants/web.constants"; - -interface FormContainerProps { - strategy: "fixed" | "absolute"; - left: number; - top: number; -} - -export const StyledFloatContainer = styled.div` - position: ${({ strategy }) => strategy || "absolute"}; - left: ${({ left }) => left}px; - top: ${({ top }) => top}px; - width: max-content; - z-index: ${Z_INDEX_FLOATING_FORM}; -`; diff --git a/packages/web/src/views/Week/components/Draft/grid/GridDraft.tsx b/packages/web/src/views/Week/components/Draft/grid/GridDraft.tsx index 8530b757a..ec382d6ec 100644 --- a/packages/web/src/views/Week/components/Draft/grid/GridDraft.tsx +++ b/packages/web/src/views/Week/components/Draft/grid/GridDraft.tsx @@ -7,7 +7,7 @@ import { type PartialMouseEvent } from "@web/common/types/util.types"; import { type Schema_GridEvent } from "@web/common/types/web.event.types"; import { getEventDragOffset } from "@web/common/utils/event/event.util"; import { EventForm } from "@web/views/Forms/EventForm/EventForm"; -import { StyledFloatContainer } from "@web/views/Forms/SomedayEventForm/styled"; +import { FloatingFormContainer } from "@web/views/Forms/SomedayEventForm/FloatingFormContainer"; import { useDraftContext } from "@web/views/Week/components/Draft/context/useDraftContext"; import { GridEvent } from "@web/views/Week/components/Event/Grid/GridEvent/GridEvent"; import { AllDayEventMemo } from "@web/views/Week/components/Grid/AllDayRow/AllDayEvent"; @@ -145,7 +145,7 @@ export const GridDraft: FC = ({ modal={false} closeOnFocusOut={false} > - = ({ titleEditingResetKey={state.draftSessionKey} titleInputRef={titleInputRef} /> - + )} From 17966b9ff18a25ea2c6004283f595dd2371e9dcd Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Thu, 18 Jun 2026 17:25:12 -0500 Subject: [PATCH 06/29] refactor(web): migrate planner sidebar to tailwind --- .../common/styles/planner-tailwind.test.ts | 27 ++++++ .../PlannerMonthPicker/PlannerMonthPicker.tsx | 52 +---------- .../SomedayEvent/SomedayEvent.tsx | 28 ++++-- .../SomedayEvent/someday-event.constants.ts | 4 + .../SomedayEvents/SomedayEvent/styled.ts | 52 ----------- .../useSomedayRefreshReserve.ts | 2 +- packages/web/src/index.css | 90 +++++++++++++++++-- .../components/EndsOnDate.tsx | 3 +- .../SomedayRecurrenceSelect.tsx | 24 ++--- 9 files changed, 149 insertions(+), 133 deletions(-) create mode 100644 packages/web/src/common/styles/planner-tailwind.test.ts create mode 100644 packages/web/src/components/PlannerSidebar/SomedayEventSections/SomedayEvents/SomedayEvent/someday-event.constants.ts delete mode 100644 packages/web/src/components/PlannerSidebar/SomedayEventSections/SomedayEvents/SomedayEvent/styled.ts diff --git a/packages/web/src/common/styles/planner-tailwind.test.ts b/packages/web/src/common/styles/planner-tailwind.test.ts new file mode 100644 index 000000000..7ebcdaf0a --- /dev/null +++ b/packages/web/src/common/styles/planner-tailwind.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, test } from "bun:test"; +import { existsSync, readFileSync } from "node:fs"; +import { join } from "node:path"; + +const webRoot = join(import.meta.dir, "../.."); + +describe("Planner Tailwind migration", () => { + test.each([ + "components/PlannerSidebar/PlannerMonthPicker/PlannerMonthPicker.tsx", + "components/PlannerSidebar/SomedayEventSections/SomedayEvents/SomedayEvent/SomedayEvent.tsx", + ])("%s has no styled-components dependency", (file) => { + expect(readFileSync(join(webRoot, file), "utf8")).not.toMatch( + /styled-components|\/styled["']/, + ); + }); + + test("removes the someday event styled module", () => { + expect( + existsSync( + join( + webRoot, + "components/PlannerSidebar/SomedayEventSections/SomedayEvents/SomedayEvent/styled.ts", + ), + ), + ).toBe(false); + }); +}); diff --git a/packages/web/src/components/PlannerSidebar/PlannerMonthPicker/PlannerMonthPicker.tsx b/packages/web/src/components/PlannerSidebar/PlannerMonthPicker/PlannerMonthPicker.tsx index 0a67c1669..abbf195ca 100644 --- a/packages/web/src/components/PlannerSidebar/PlannerMonthPicker/PlannerMonthPicker.tsx +++ b/packages/web/src/components/PlannerSidebar/PlannerMonthPicker/PlannerMonthPicker.tsx @@ -1,5 +1,4 @@ import { type FC, useEffect, useRef, useState } from "react"; -import styled from "styled-components"; import dayjs, { type Dayjs } from "@core/util/date/dayjs"; import { ID_DATEPICKER_SIDEBAR } from "@web/common/constants/web.constants"; import { theme } from "@web/common/styles/theme"; @@ -19,51 +18,6 @@ const plannerMonthPickerClassName = const headerActionsClassName = "!ml-2.5"; -const PlannerMonthPickerFieldset = styled.fieldset` - .react-datepicker__month-container, - .react-datepicker__month { - overflow: visible !important; - } - - .react-datepicker__day--selected { - isolation: isolate !important; - background-color: transparent !important; - position: relative !important; - color: var(--color-text-dark) !important; - } - - .react-datepicker__day--selected::before { - position: absolute; - inset: -2px; - z-index: -1; - border-radius: var(--radius-default); - background-color: var(--color-accent-primary); - content: ""; - } - - .react-datepicker__week:has(.react-datepicker__day--selected) { - position: relative !important; - } - - .react-datepicker__week:has(.react-datepicker__day--selected)::before { - position: absolute; - inset: 0 -3px; - border-radius: var(--radius-default); - background-color: color-mix( - in srgb, - var(--color-panel-scrollbar-active) 50%, - transparent - ); - content: ""; - } - - .react-datepicker__week:has(.react-datepicker__day--selected) - .react-datepicker__day { - position: relative !important; - z-index: 10 !important; - } -`; - export const PlannerMonthPicker: FC = ({ monthsShown, onSelectDate, @@ -97,8 +51,8 @@ export const PlannerMonthPicker: FC = ({ }; return ( - @@ -139,6 +93,6 @@ export const PlannerMonthPicker: FC = ({ view="sidebar" withTodayButton={false} /> - + ); }; diff --git a/packages/web/src/components/PlannerSidebar/SomedayEventSections/SomedayEvents/SomedayEvent/SomedayEvent.tsx b/packages/web/src/components/PlannerSidebar/SomedayEventSections/SomedayEvents/SomedayEvent/SomedayEvent.tsx index 726cdf961..63e1378ae 100644 --- a/packages/web/src/components/PlannerSidebar/SomedayEventSections/SomedayEvents/SomedayEvent/SomedayEvent.tsx +++ b/packages/web/src/components/PlannerSidebar/SomedayEventSections/SomedayEvents/SomedayEvent/SomedayEvent.tsx @@ -2,11 +2,16 @@ import { type KeyboardEvent, type Ref } from "react"; import { type Priorities } from "@core/constants/core.constants"; import { type Schema_Event } from "@core/types/event.types"; import { DATA_EVENT_ELEMENT_ID } from "@web/common/constants/web.constants"; +import { type CSSVariables } from "@web/common/styles/css.types"; +import { colorByPriority } from "@web/common/styles/theme.util"; import { type Actions_Sidebar } from "@web/components/PlannerSidebar/draft/hooks/useSidebarActions"; import { type SomedayInteractionCategory } from "@web/components/PlannerSidebar/SomedayEventSections/interaction/registry/somedayEventRegistry"; import { type Props_DraftForm } from "@web/views/Week/components/Draft/hooks/state/useDraftForm"; import { SomedayEventRectangle } from "../SomedayEventContainer/SomedayEventRectangle"; -import { StyledNewSomedayEvent } from "./styled"; +import { + SOMEDAY_EVENT_ROW_HEIGHT, + SOMEDAY_EVENT_ROW_VERTICAL_MARGIN, +} from "./someday-event.constants"; interface Props { category: SomedayInteractionCategory; @@ -49,29 +54,40 @@ export const SomedayEvent = ({ onClick(); }; + const tint = (percent: number) => + `color-mix(in srgb, ${colorByPriority[priority]} ${percent}%, transparent)`; const somedayEventProps = { [DATA_EVENT_ELEMENT_ID]: event._id, "aria-hidden": isDragging || undefined, - isDragging, - isDrafting, onBlur, onClick, onFocus, onKeyDown: handleKeyDown, - priority, role: "button", ref: interactionRef, tabIndex: isDragging ? -1 : 0, }; return ( - +
- +
); }; diff --git a/packages/web/src/components/PlannerSidebar/SomedayEventSections/SomedayEvents/SomedayEvent/someday-event.constants.ts b/packages/web/src/components/PlannerSidebar/SomedayEventSections/SomedayEvents/SomedayEvent/someday-event.constants.ts new file mode 100644 index 000000000..9ee571285 --- /dev/null +++ b/packages/web/src/components/PlannerSidebar/SomedayEventSections/SomedayEvents/SomedayEvent/someday-event.constants.ts @@ -0,0 +1,4 @@ +export const SOMEDAY_EVENT_ROW_HEIGHT = 30; +export const SOMEDAY_EVENT_ROW_VERTICAL_MARGIN = 2; +export const SOMEDAY_EVENT_ROW_FOOTPRINT = + SOMEDAY_EVENT_ROW_HEIGHT + SOMEDAY_EVENT_ROW_VERTICAL_MARGIN * 2; diff --git a/packages/web/src/components/PlannerSidebar/SomedayEventSections/SomedayEvents/SomedayEvent/styled.ts b/packages/web/src/components/PlannerSidebar/SomedayEventSections/SomedayEvents/SomedayEvent/styled.ts deleted file mode 100644 index d0521efd5..000000000 --- a/packages/web/src/components/PlannerSidebar/SomedayEventSections/SomedayEvents/SomedayEvent/styled.ts +++ /dev/null @@ -1,52 +0,0 @@ -import styled from "styled-components"; -import { type Priorities } from "@core/constants/core.constants"; -import { colorByPriority } from "@web/common/styles/theme.util"; - -const SOMEDAY_EVENT_ROW_HEIGHT = 30; -const SOMEDAY_EVENT_ROW_VERTICAL_MARGIN = 2; -export const SOMEDAY_EVENT_ROW_FOOTPRINT = - SOMEDAY_EVENT_ROW_HEIGHT + SOMEDAY_EVENT_ROW_VERTICAL_MARGIN * 2; - -const getPriorityTint = (priority: Priorities, mixPercent: number) => - `color-mix(in srgb, ${colorByPriority[priority]} ${mixPercent}%, transparent)`; - -export interface Props { - priority: Priorities; - isDrafting: boolean; - isDragging?: boolean; -} - -export const StyledNewSomedayEvent = styled.div` - background: ${({ isDrafting, isDragging, priority }) => { - if (isDrafting) { - return getPriorityTint(priority, isDragging ? 45 : 35); - } - - return getPriorityTint(priority, 15); - }}; - - border-radius: 2px; - color: ${({ theme }) => theme.color.text.lighter}; - height: ${SOMEDAY_EVENT_ROW_HEIGHT}px; - margin: ${SOMEDAY_EVENT_ROW_VERTICAL_MARGIN}px 0; - opacity: ${({ isDragging }) => (isDragging ? 0 : 1)}; - font-size: 12px; - padding: 3px 8px; - pointer-events: ${({ isDragging }) => (isDragging ? "none" : "auto")}; - transition: - background-color 0.2s, - opacity 0.12s, - box-shadow 0.2s; - width: 100%; - - cursor: ${({ isDragging }) => (isDragging ? "grabbing" : "grab")}; - - &:hover { - background: ${({ priority }) => getPriorityTint(priority, 25)}; - } - - &:focus-visible { - outline: 2px solid ${({ theme }) => theme.color.text.accent}; - outline-offset: 2px; - } -`; diff --git a/packages/web/src/components/PlannerSidebar/SomedayEventSections/SomedayEvents/SomedayEventsContainer/useSomedayRefreshReserve.ts b/packages/web/src/components/PlannerSidebar/SomedayEventSections/SomedayEvents/SomedayEventsContainer/useSomedayRefreshReserve.ts index d43c8c768..09ef83c5d 100644 --- a/packages/web/src/components/PlannerSidebar/SomedayEventSections/SomedayEvents/SomedayEventsContainer/useSomedayRefreshReserve.ts +++ b/packages/web/src/components/PlannerSidebar/SomedayEventSections/SomedayEvents/SomedayEventsContainer/useSomedayRefreshReserve.ts @@ -1,5 +1,5 @@ import { useEffect, useRef } from "react"; -import { SOMEDAY_EVENT_ROW_FOOTPRINT } from "../SomedayEvent/styled"; +import { SOMEDAY_EVENT_ROW_FOOTPRINT } from "../SomedayEvent/someday-event.constants"; interface Result { reservedMinHeight: number | undefined; diff --git a/packages/web/src/index.css b/packages/web/src/index.css index fbae92be3..250cc9cba 100644 --- a/packages/web/src/index.css +++ b/packages/web/src/index.css @@ -671,9 +671,10 @@ } @utility c-actions-menu-item { - @apply flex w-full cursor-pointer items-center gap-2 border-0 px-2 py-1 text-left text-m outline-none; + @apply flex w-full cursor-pointer items-center gap-2 border-0 px-2 py-1 text-left outline-none; background-color: var(--actions-menu-item-bg); color: var(--compass-color-text-dark); + font-size: var(--font-size-m); &:hover, &:focus-visible { @@ -687,17 +688,24 @@ @apply rounded-sm px-5 py-[18px] text-xl transition-[var(--compass-transition-default)]; background: var(--event-form-bg); box-shadow: 0 5px 5px var(--compass-color-shadow-default); + z-index: 1; +} + +@utility c-floating-form-container { + width: max-content; } @utility c-event-form-title { - @apply bg-transparent text-5xl font-semibold transition-all duration-300; + @apply bg-transparent font-semibold transition-all duration-300; + font-size: var(--font-size-5xl); &:hover { background-color: var(--compass-color-border-primary); } } @utility c-event-form-description { - @apply relative max-h-[180px] border-hidden bg-transparent text-xxl font-normal transition-all duration-300; + @apply relative max-h-[180px] border-hidden bg-transparent font-normal transition-all duration-300; + font-size: var(--font-size-xxl); width: calc(100% - 20px); &:hover { background-color: var(--compass-color-border-primary); @@ -706,7 +714,8 @@ } @utility c-time-picker { - @apply relative min-w-[90px] text-lg; + @apply relative min-w-[90px]; + font-size: var(--font-size-l); & span[aria-live="polite"], & .timepicker__indicators { display: none; @@ -767,8 +776,9 @@ } @utility c-recurrence-toggle { - @apply inline-flex items-center gap-1 rounded-sm border border-transparent bg-transparent px-2 py-0.5 text-m transition-all duration-300; + @apply inline-flex items-center gap-1 rounded-sm border border-transparent bg-transparent px-2 py-0.5 transition-all duration-300; color: var(--compass-color-text-dark); + font-size: var(--font-size-m); &[data-repeat="false"] { color: var(--compass-color-text-dark-placeholder); opacity: 0.85; @@ -790,9 +800,10 @@ } @utility c-recurrence-weekday { - @apply size-6 cursor-pointer rounded-full border text-m transition-all duration-300; + @apply size-6 cursor-pointer rounded-full border transition-all duration-300; border-color: var(--compass-color-border-primary-dark); background: var(--weekday-bg); + font-size: var(--font-size-m); &[data-selected="true"] { background: var(--weekday-selected-bg); color: var(--weekday-selected-text); @@ -807,8 +818,9 @@ } @utility c-recurrence-interval { - @apply ml-1 h-[38px] w-8 rounded-sm border border-transparent px-1 text-center text-s transition-all duration-300; + @apply ml-1 h-[38px] w-8 rounded-sm border border-transparent px-1 text-center transition-all duration-300; background: var(--recurrence-bg); + font-size: var(--font-size-s); &::-webkit-outer-spin-button, &::-webkit-inner-spin-button { margin: 0; @@ -836,3 +848,67 @@ box-shadow: 0 0 0 2px var(--compass-color-border-primary-dark); } } + +@utility c-someday-recurrence-value { + @apply inline-flex items-center gap-1; + color: var(--compass-color-text-dark); + font-size: var(--font-size-m); + + &[data-dimmed="true"] { + color: var(--compass-color-text-dark-placeholder); + } +} + +@utility c-planner-month-picker { + & .react-datepicker__month-container, + & .react-datepicker__month { + overflow: visible !important; + } + & .react-datepicker__day--selected { + position: relative !important; + isolation: isolate !important; + background: transparent !important; + color: var(--compass-color-text-dark) !important; + } + & .react-datepicker__day--selected::before { + position: absolute; + inset: -2px; + z-index: -1; + border-radius: var(--radius-default); + background: var(--compass-color-accent-primary); + content: ""; + } + & .react-datepicker__week:has(.react-datepicker__day--selected) { + position: relative !important; + } + & .react-datepicker__week:has(.react-datepicker__day--selected)::before { + position: absolute; + inset: 0 -3px; + border-radius: var(--radius-default); + background: color-mix( + in srgb, + var(--compass-color-panel-scrollbar-active) 50%, + transparent + ); + content: ""; + } + & + .react-datepicker__week:has(.react-datepicker__day--selected) + .react-datepicker__day { + position: relative !important; + z-index: 10 !important; + } +} + +@utility c-someday-event { + @apply w-full cursor-grab rounded-[2px] px-2 py-[3px] text-xs text-text-lighter transition-[background-color_.2s,opacity_.12s,box-shadow_.2s] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent-primary; + background: var(--someday-event-bg); + &[data-dragging="true"] { + pointer-events: none; + cursor: grabbing; + opacity: 0; + } + &:hover { + background: var(--someday-event-hover-bg); + } +} diff --git a/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/components/EndsOnDate.tsx b/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/components/EndsOnDate.tsx index 905c05c4e..3bbd290f0 100644 --- a/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/components/EndsOnDate.tsx +++ b/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/components/EndsOnDate.tsx @@ -2,7 +2,6 @@ import type React from "react"; import { useMemo, useState } from "react"; import { darken } from "@core/util/color.utils"; import { parseCompassEventDate } from "@core/util/event/event.util"; -import { theme } from "@web/common/styles/theme"; import { DatePicker } from "@web/components/DatePicker/DatePicker"; import { Flex } from "@web/components/Flex/Flex"; import { Text } from "@web/components/Text/Text"; @@ -33,7 +32,7 @@ export const EndsOnDate = ({ >(null); useEffect(() => { @@ -109,18 +108,11 @@ export const SomedayRecurrenceSelect = ({ return ( - + {isDoNotRepeat ? DO_NOT_REPEAT_OPTION.label @@ -153,19 +145,19 @@ export const SomedayRecurrenceSelect = ({ borderRadius: theme.shape.borderRadius, fontSize, borderColor: state.isFocused - ? selectTheme.color.border.primaryDark + ? "var(--compass-color-border-primary-dark)" : baseStyles.borderColor, boxShadow: state.isFocused - ? `0 0 0 1px ${selectTheme.color.border.primaryDark}` + ? "0 0 0 1px var(--compass-color-border-primary-dark)" : baseStyles.boxShadow, }), valueContainer: (baseStyles) => ({ ...baseStyles, - paddingLeft: selectTheme.spacing.s, + paddingLeft: "0.5rem", }), placeholder: (baseStyles) => ({ ...baseStyles, - color: selectTheme.color.text.dark, + color: "var(--compass-color-text-dark)", }), indicatorSeparator: () => ({ visibility: "hidden", @@ -186,8 +178,8 @@ export const SomedayRecurrenceSelect = ({ ? bgDark : undefined, color: isDisabled - ? selectTheme.color.text.lightInactive - : theme.color.text.dark, + ? "var(--compass-color-text-light-inactive)" + : "var(--compass-color-text-dark)", cursor: isDisabled ? "not-allowed" : "default", ":active": { ...styles[":active"], From 50e638d000a9bb8ab2a14706b94d40bad044a6d3 Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Thu, 18 Jun 2026 17:29:27 -0500 Subject: [PATCH 07/29] refactor(web): migrate calendar grid to tailwind --- .../components/CalendarAllDayRow.tsx | 75 ++--- .../components/CalendarTimedGrid.tsx | 134 +++------ .../src/common/styles/week-tailwind.test.ts | 35 +++ .../PlannerMonthPicker/PlannerMonthPicker.tsx | 5 +- packages/web/src/index.css | 272 ++++++++++++++++++ .../web/src/views/Day/view/DayViewContent.tsx | 13 +- packages/web/src/views/Week/WeekView.tsx | 17 +- .../Grid/AllDayRow/AllDayEvents.tsx | 8 +- .../Week/components/Grid/AllDayRow/styled.ts | 45 --- .../Week/components/Grid/Columns/styled.ts | 14 - .../EdgeNavigationIndicators.tsx | 17 +- .../EdgeNavigationIndicators/styled.ts | 40 --- .../components/Grid/WeekGridScrollArea.tsx | 10 +- .../Week/components/Header/DayLabels.tsx | 55 +--- .../components/Header/Reminder/Reminder.tsx | 111 ++++--- .../Week/components/Header/Reminder/styled.ts | 220 -------------- packages/web/src/views/Week/styled.tsx | 59 ---- 17 files changed, 490 insertions(+), 640 deletions(-) create mode 100644 packages/web/src/common/styles/week-tailwind.test.ts delete mode 100644 packages/web/src/views/Week/components/Grid/AllDayRow/styled.ts delete mode 100644 packages/web/src/views/Week/components/Grid/Columns/styled.ts delete mode 100644 packages/web/src/views/Week/components/Grid/MainGrid/EdgeNavigationIndicators/styled.ts delete mode 100644 packages/web/src/views/Week/components/Header/Reminder/styled.ts delete mode 100644 packages/web/src/views/Week/styled.tsx diff --git a/packages/web/src/common/calendar-grid/components/CalendarAllDayRow.tsx b/packages/web/src/common/calendar-grid/components/CalendarAllDayRow.tsx index 5067c30d9..527d68879 100644 --- a/packages/web/src/common/calendar-grid/components/CalendarAllDayRow.tsx +++ b/packages/web/src/common/calendar-grid/components/CalendarAllDayRow.tsx @@ -4,7 +4,6 @@ import { type ReactNode, type RefCallback, } from "react"; -import styled from "styled-components"; import { CALENDAR_EVENT_WIDTH_MINIMUM, CALENDAR_GRID_MARGIN_LEFT, @@ -16,6 +15,7 @@ import { ID_ALLDAY_COLUMNS, ID_GRID_ALLDAY_ROW, } from "@web/common/constants/web.constants"; +import { type CSSVariables } from "@web/common/styles/css.types"; import { Flex } from "@web/components/Flex/Flex"; interface CalendarAllDayRowProps { @@ -38,53 +38,6 @@ const getAllDayRowHeight = (gridOffsetTopPx: number) => { return `${gridRowHeight} / ${interval}`; }; -const StyledAllDayColumns = styled.div<{ $visibleDateCount: number }>` - display: grid; - grid-template-columns: ${({ $visibleDateCount }) => - `repeat(${$visibleDateCount}, minmax(${CALENDAR_EVENT_WIDTH_MINIMUM}px, 1fr))`}; - height: 100%; - left: ${CALENDAR_GRID_MARGIN_LEFT}px; - position: absolute; - top: 0; - width: calc(100% - ${CALENDAR_GRID_MARGIN_LEFT}px); - - &::before { - background: ${({ theme }) => theme.color.gridLine.primary}; - bottom: 0; - content: ""; - height: 2px; - left: 0; - pointer-events: none; - position: absolute; - right: 0; - } -`; - -const StyledAllDayRow = styled(Flex)<{ - $gridOffsetTopPx: number; - $rowsCount: number; -}>` - flex-shrink: 0; - height: ${({ $gridOffsetTopPx, $rowsCount }) => { - const allDayRowHeight = getAllDayRowHeight($gridOffsetTopPx); - - return `calc(${allDayRowHeight} * 2 + ${ - $rowsCount * 2 || 1 - } * ${allDayRowHeight})`; - }}; - position: relative; - width: 100%; -`; - -const StyledDateColumn = styled.div` - border-left: ${({ theme }) => `1px solid ${theme.color.gridLine.primary}`}; - box-sizing: border-box; - display: block; - height: 100%; - min-width: ${CALENDAR_EVENT_WIDTH_MINIMUM}px; - position: relative; -`; - export const CalendarAllDayRow: FC = ({ allDayColumnsRef, allDayRowRef, @@ -96,28 +49,38 @@ export const CalendarAllDayRow: FC = ({ rowId = ID_GRID_ALLDAY_ROW, visibleDates, }) => ( - - {visibleDates.map(({ date, key }) => ( - ))} - +
{eventsLayer} - + ); diff --git a/packages/web/src/common/calendar-grid/components/CalendarTimedGrid.tsx b/packages/web/src/common/calendar-grid/components/CalendarTimedGrid.tsx index 005639a9a..05acc0c17 100644 --- a/packages/web/src/common/calendar-grid/components/CalendarTimedGrid.tsx +++ b/packages/web/src/common/calendar-grid/components/CalendarTimedGrid.tsx @@ -7,7 +7,6 @@ import { useMemo, useState, } from "react"; -import styled from "styled-components"; import dayjs, { type Dayjs } from "@core/util/date/dayjs"; import { CALENDAR_EVENT_WIDTH_MINIMUM, @@ -21,6 +20,7 @@ import { ID_GRID_MAIN, ZIndex, } from "@web/common/constants/web.constants"; +import { type CSSVariables } from "@web/common/styles/css.types"; import { blueGradient } from "@web/common/styles/theme.util"; import { getColorsByHour, @@ -41,85 +41,6 @@ interface CalendarTimedGridProps { visibleDates: CalendarGridVisibleDate[]; } -const StyledGridRow = styled(Flex)` - height: calc(100% / ${CALENDAR_TIMED_VISIBLE_HOURS}); - border-bottom: ${({ theme }) => `1px solid ${theme.color.gridLine.primary}`}; - width: 100%; - position: relative; - - & > span { - position: absolute; - bottom: -5px; - left: -${CALENDAR_GRID_MARGIN_LEFT}px; - } -`; - -const StyledGridWithTimeLabels = styled.div` - height: 100%; - left: ${CALENDAR_GRID_MARGIN_LEFT}px; - position: absolute; - width: calc(100% - ${CALENDAR_GRID_MARGIN_LEFT}px); -`; - -const StyledMainGrid = styled.div` - --scrollbar-width: 0px; - flex: 1; - min-height: 0; - overflow-x: hidden; - overflow-y: auto; - position: relative; - width: 100%; -`; - -const StyledTimedColumns = styled.div<{ $visibleDateCount: number }>` - display: grid; - grid-template-columns: ${({ $visibleDateCount }) => - `repeat(${$visibleDateCount}, minmax(${CALENDAR_EVENT_WIDTH_MINIMUM}px, 1fr))`}; - height: calc(24 * 100% / ${CALENDAR_TIMED_VISIBLE_HOURS}); - left: ${CALENDAR_GRID_MARGIN_LEFT}px; - position: absolute; - top: 0; - width: calc(100% - ${CALENDAR_GRID_MARGIN_LEFT}px); -`; - -const StyledDateColumn = styled.div<{ $isPast: boolean }>` - background: ${({ $isPast, theme }) => - $isPast ? theme.color.bg.secondary : "transparent"}; - border-left: ${({ theme }) => `1px solid ${theme.color.gridLine.primary}`}; - box-sizing: border-box; - height: 100%; - min-width: ${CALENDAR_EVENT_WIDTH_MINIMUM}px; - position: relative; -`; - -const StyledTimesLabel = styled.div<{ color: string }>` - color: ${({ color }) => color}; -`; - -const StyledDayTimes = styled.div` - height: 100%; - position: absolute; - top: calc(100% / ${CALENDAR_TIMED_VISIBLE_HOURS} + -5px); - z-index: ${ZIndex.LAYER_1}; - - & > div { - height: calc(100% / ${CALENDAR_TIMED_VISIBLE_HOURS}); - - & > span { - display: block; - } - } -`; - -const StyledNowLine = styled.div<{ top: number }>` - background: ${blueGradient}; - height: 1px; - position: absolute; - top: ${({ top }) => top}%; - width: 100%; - z-index: ${ZIndex.LAYER_2}; -`; - export const CalendarTimedGrid: FC = ({ columnsId = ID_GRID_COLUMNS_TIMED, eventsLayer, @@ -135,43 +56,53 @@ export const CalendarTimedGrid: FC = ({ ); return ( - - {isTodayVisible ? : null} {visibleDates.map(({ date, key }) => ( - ))} - + - +
{getHourLabels(true).map((dayTime) => ( - ))} - +
{eventsLayer} -
+ ); }; @@ -193,13 +124,13 @@ const CalendarTimeColumn = () => { }, [currentHour]); return ( - +
{hourLabels.map((label, index) => ( - +
{label} - +
))} - +
); }; @@ -216,5 +147,16 @@ const CalendarNowLine = () => { return () => clearInterval(interval); }, []); - return ; + return ( +
+ ); }; diff --git a/packages/web/src/common/styles/week-tailwind.test.ts b/packages/web/src/common/styles/week-tailwind.test.ts new file mode 100644 index 000000000..57350d4be --- /dev/null +++ b/packages/web/src/common/styles/week-tailwind.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, test } from "bun:test"; +import { existsSync, readFileSync } from "node:fs"; +import { join } from "node:path"; + +const webRoot = join(import.meta.dir, "../.."); +const productionFiles = [ + "views/Week/WeekView.tsx", + "views/Week/components/Grid/WeekGridScrollArea.tsx", + "views/Week/components/Grid/AllDayRow/AllDayEvents.tsx", + "views/Week/components/Grid/AllDayRow/AllDayRow.tsx", + "views/Week/components/Header/DayLabels.tsx", + "views/Week/components/Header/Reminder/Reminder.tsx", + "views/Week/components/Grid/MainGrid/EdgeNavigationIndicators/EdgeNavigationIndicators.tsx", +]; +const styledModules = [ + "views/Week/styled.tsx", + "views/Week/components/Grid/Columns/styled.ts", + "views/Week/components/Grid/AllDayRow/styled.ts", + "views/Week/components/Header/Reminder/styled.ts", + "views/Week/components/Grid/MainGrid/EdgeNavigationIndicators/styled.ts", +]; + +describe("Week Tailwind migration", () => { + test.each( + productionFiles, + )("%s has no styled-components dependency", (file) => { + expect(readFileSync(join(webRoot, file), "utf8")).not.toMatch( + /styled-components|\/styled["']/, + ); + }); + + test.each(styledModules)("removes %s", (file) => { + expect(existsSync(join(webRoot, file))).toBe(false); + }); +}); diff --git a/packages/web/src/components/PlannerSidebar/PlannerMonthPicker/PlannerMonthPicker.tsx b/packages/web/src/components/PlannerSidebar/PlannerMonthPicker/PlannerMonthPicker.tsx index abbf195ca..51863e8f7 100644 --- a/packages/web/src/components/PlannerSidebar/PlannerMonthPicker/PlannerMonthPicker.tsx +++ b/packages/web/src/components/PlannerSidebar/PlannerMonthPicker/PlannerMonthPicker.tsx @@ -1,7 +1,6 @@ import { type FC, useEffect, useRef, useState } from "react"; import dayjs, { type Dayjs } from "@core/util/date/dayjs"; import { ID_DATEPICKER_SIDEBAR } from "@web/common/constants/web.constants"; -import { theme } from "@web/common/styles/theme"; import { DatePicker } from "@web/components/DatePicker/DatePicker"; import { SidebarIcon } from "@web/components/Icons/Sidebar"; import { TooltipWrapper } from "@web/components/Tooltip/TooltipWrapper"; @@ -68,8 +67,8 @@ export const PlannerMonthPicker: FC = ({ onClick={onToggleSidebar} shortcut="[" > - - + + ) : null diff --git a/packages/web/src/index.css b/packages/web/src/index.css index 250cc9cba..9de9e0139 100644 --- a/packages/web/src/index.css +++ b/packages/web/src/index.css @@ -912,3 +912,275 @@ background: var(--someday-event-hover-bg); } } + +@utility c-week-grid-scroller { + @apply h-full w-full overflow-x-auto overflow-y-hidden; + overscroll-behavior-x: contain; + scrollbar-width: none; + &::-webkit-scrollbar { + display: none; + width: 0; + height: 0; + } + &:focus-visible { + outline: 1px solid var(--compass-color-accent-primary); + outline-offset: -1px; + } +} + +@utility c-week-grid-track { + @apply relative flex h-full w-full min-w-[704px] flex-col; + container: week-grid-track / inline-size; +} + +@utility c-week-columns { + @apply absolute top-0 grid; + left: 50px; + width: calc(100% - 50px); + grid-template-columns: repeat(7, minmax(80px, 1fr)); +} + +@utility c-week-day-labels { + @apply relative mt-2.5 min-h-8 w-full; + + & .week-day-number { + font-size: clamp(var(--font-size-xl), 2.7cqw, var(--font-size-xxl)); + line-height: 1; + } + + & .week-day-name { + font-size: clamp(var(--font-size-m), 2cqw, var(--font-size-l)); + line-height: 1; + } +} + +@utility c-week-edge-zone { + @apply pointer-events-none absolute inset-y-0 z-1 transition-[width] duration-50 ease-linear; + width: var(--edge-width); + + &[data-position="left"] { + left: var(--grid-margin-left); + background: linear-gradient( + to right, + rgb(59 130 246 / var(--edge-opacity)), + transparent + ); + } + + &[data-position="right"] { + right: 0; + background: linear-gradient( + to left, + rgb(59 130 246 / var(--edge-opacity)), + transparent + ); + } +} + +@keyframes c-reminder-underline-draw { + from { + stroke-dashoffset: 1000; + opacity: 0.7; + } + to { + stroke-dashoffset: 0; + opacity: 1; + } +} + +@keyframes c-reminder-fade-in { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes c-reminder-gradient-wave { + 0%, + 100% { + background-position: 0% 50%; + } + 50% { + background-position: 100% 50%; + } +} + +@keyframes c-reminder-shadow { + 0%, + 100% { + filter: drop-shadow(0 0 2px var(--compass-color-bg-primary)); + } + 50% { + filter: drop-shadow(0 0 4px var(--compass-color-bg-secondary)); + } +} + +@utility c-reminder-container { + @apply z-1 mx-auto flex h-full w-full items-center justify-center; +} + +@utility c-reminder-wrapper { + @apply relative inline-block pb-[18px]; +} + +@utility c-reminder-char-counter { + @apply absolute right-0 bottom-[-2px] left-0 w-full text-center text-xs opacity-80; + color: var(--compass-color-text-light-inactive); + &[data-near-limit="true"] { + color: var(--compass-color-status-warning); + } +} + +@utility c-reminder-placeholder { + @apply relative cursor-pointer px-2.5 text-center text-[28px] italic; + color: var(--compass-color-text-light-inactive); + font-family: "Caveat", cursive; + animation: c-reminder-fade-in 0.5s ease-out; + &:hover { + color: var(--compass-color-text-light); + transition: color 0.3s ease; + } +} + +@utility c-reminder-text { + @apply relative min-w-[200px] max-w-[500px] cursor-pointer px-2.5 text-center font-semibold whitespace-pre-wrap break-words; + color: transparent; + background: linear-gradient( + 90deg, + var(--compass-color-gradient-accent-light-start), + var(--compass-color-gradient-accent-light-end) + ); + background-size: 200% 200%; + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + font-family: "Caveat", cursive; + font-size: 28px; + line-height: 1.3; + filter: drop-shadow(0 0 2px var(--compass-color-bg-primary)); + animation: + c-reminder-fade-in 0.5s ease-out, + c-reminder-gradient-wave 15s ease infinite; + &[data-length="medium"] { + font-size: 24px; + } + &[data-length="long"] { + font-size: 22px; + } + &[data-editing="true"] { + max-height: 80px; + cursor: text; + overflow: hidden; + outline: none; + } + &[data-editing="false"] { + max-height: 80px; + overflow-y: auto; + } + @media (prefers-reduced-motion: no-preference) { + animation: + c-reminder-shadow 5s ease-in-out infinite, + c-reminder-fade-in 0.5s ease-out, + c-reminder-gradient-wave 15s ease infinite; + } +} + +@utility c-reminder-underline { + @apply pointer-events-none absolute bottom-0 left-[-5px] h-5 opacity-0; + width: calc(100% + 10px); + &[data-visible="true"] { + opacity: 1; + } + & path { + fill: none; + stroke-width: 3; + stroke-linecap: round; + stroke-linejoin: round; + stroke-dasharray: 1000; + stroke-dashoffset: 1000; + } + &[data-visible="true"] path { + animation: c-reminder-underline-draw 0.8s ease-out forwards; + } +} + +@utility c-reminder-placeholder-underline { + @apply c-reminder-underline; + & path { + stroke-width: 2.5; + } +} + +@utility c-calendar-all-day-columns { + @apply absolute top-0 grid h-full; + left: var(--calendar-grid-margin-left); + width: calc(100% - var(--calendar-grid-margin-left)); + grid-template-columns: repeat( + var(--calendar-column-count), + minmax(var(--calendar-column-min-width), 1fr) + ); + &::before { + position: absolute; + right: 0; + bottom: 0; + left: 0; + height: 2px; + pointer-events: none; + background: var(--compass-color-grid-line-primary); + content: ""; + } +} + +@utility c-calendar-date-column { + @apply relative block h-full box-border border-l; + min-width: var(--calendar-column-min-width); + border-color: var(--compass-color-grid-line-primary); +} + +@utility c-calendar-main-grid { + @apply relative min-h-0 w-full flex-1 overflow-x-hidden overflow-y-auto; + --scrollbar-width: 0px; +} +@utility c-calendar-timed-columns { + @apply absolute top-0 grid; + left: var(--calendar-grid-margin-left); + width: calc(100% - var(--calendar-grid-margin-left)); + height: calc(24 * 100% / var(--calendar-visible-hours)); + grid-template-columns: repeat( + var(--calendar-column-count), + minmax(var(--calendar-column-min-width), 1fr) + ); +} +@utility c-calendar-grid-rows { + position: absolute; + left: 50px; + width: calc(100% - 50px); + height: 100%; +} +@utility c-calendar-grid-row { + @apply relative w-full border-b border-grid-line-primary; + height: calc(100% / 13); + & > span { + position: absolute; + bottom: -5px; + left: -50px; + } +} +@utility c-calendar-day-times { + @apply absolute h-full; + top: calc(100% / 13 - 5px); + z-index: 1; + & > div { + height: calc(100% / 13); + } + & > div > span { + display: block; + } +} +@utility c-calendar-now-line { + @apply absolute h-px w-full; +} diff --git a/packages/web/src/views/Day/view/DayViewContent.tsx b/packages/web/src/views/Day/view/DayViewContent.tsx index 3a018990a..0fb22c97f 100644 --- a/packages/web/src/views/Day/view/DayViewContent.tsx +++ b/packages/web/src/views/Day/view/DayViewContent.tsx @@ -1,5 +1,6 @@ import { memo, useCallback, useMemo } from "react"; import dayjs from "@core/util/date/dayjs"; +import { ID_MAIN } from "@web/common/constants/web.constants"; import { CompassDOMEvents, compassEventEmitter, @@ -29,7 +30,6 @@ import { } from "@web/views/Day/util/day.shortcut.util"; import { Dedication } from "@web/views/Week/components/Dedication/Dedication"; import { useRefetch } from "@web/views/Week/hooks/useRefetch"; -import { Styled, StyledCalendar } from "@web/views/Week/styled"; export const DayViewContent = memo(() => { const dispatch = useAppDispatch(); @@ -160,7 +160,7 @@ export const DayViewContent = memo(() => { }); return ( - +
@@ -179,7 +179,10 @@ export const DayViewContent = memo(() => { /> ) : null} - +
{
-
- +
+
); }); diff --git a/packages/web/src/views/Week/WeekView.tsx b/packages/web/src/views/Week/WeekView.tsx index 85d219aaa..a1a53d5eb 100644 --- a/packages/web/src/views/Week/WeekView.tsx +++ b/packages/web/src/views/Week/WeekView.tsx @@ -1,4 +1,5 @@ import { useCallback, useMemo } from "react"; +import { ID_MAIN } from "@web/common/constants/web.constants"; import { ContextMenuWrapper } from "@web/components/ContextMenu/GridContextMenuWrapper"; import { SidebarDraftProvider } from "@web/components/PlannerSidebar/draft/context/SidebarDraftProvider"; import { PlannerSidebar } from "@web/components/PlannerSidebar/PlannerSidebar"; @@ -25,7 +26,6 @@ import { useRefetch } from "@web/views/Week/hooks/useRefetch"; import { useToday } from "@web/views/Week/hooks/useToday"; import { useWeek } from "@web/views/Week/hooks/useWeek"; import { WeekInteractionCoordinator } from "@web/views/Week/interaction/WeekInteractionCoordinator"; -import { Styled, StyledCalendar, WeekGridTrack } from "@web/views/Week/styled"; export const WeekView = () => { useRefetch(); @@ -126,7 +126,7 @@ export const WeekView = () => { ); return ( - +
@@ -157,11 +157,14 @@ export const WeekView = () => { /> ) : null} - +
- +
{ /> - +
- +
- +
); }; diff --git a/packages/web/src/views/Week/components/Grid/AllDayRow/AllDayEvents.tsx b/packages/web/src/views/Week/components/Grid/AllDayRow/AllDayEvents.tsx index 8ea44a7b4..ecd4fa365 100644 --- a/packages/web/src/views/Week/components/Grid/AllDayRow/AllDayEvents.tsx +++ b/packages/web/src/views/Week/components/Grid/AllDayRow/AllDayEvents.tsx @@ -9,7 +9,6 @@ import { selectIsGetWeekEventsProcessingWithReason } from "@web/ducks/events/sel import { draftSlice } from "@web/ducks/events/slices/draft.slice"; import { useAppDispatch, useAppSelector } from "@web/store/store.hooks"; import { AllDayEventMemo } from "@web/views/Week/components/Grid/AllDayRow/AllDayEvent"; -import { StyledEvents } from "@web/views/Week/components/Grid/AllDayRow/styled"; import { type Measurements_Grid } from "@web/views/Week/hooks/grid/useGridLayout"; import { type WeekProps } from "@web/views/Week/hooks/useWeek"; import { @@ -54,7 +53,10 @@ export const AllDayEvents = ({ isProcessing && reason === Week_AsyncStateContextReason.WEEK_VIEW_CHANGE; return ( - +
{!isLoadingWeekView && allDayEvents.map((event: Schema_GridEvent) => { const isPending = Boolean( @@ -75,7 +77,7 @@ export const AllDayEvents = ({ /> ); })} - +
); }; diff --git a/packages/web/src/views/Week/components/Grid/AllDayRow/styled.ts b/packages/web/src/views/Week/components/Grid/AllDayRow/styled.ts deleted file mode 100644 index 8ff0a4766..000000000 --- a/packages/web/src/views/Week/components/Grid/AllDayRow/styled.ts +++ /dev/null @@ -1,45 +0,0 @@ -import styled from "styled-components"; -import { Flex } from "@web/components/Flex/Flex"; -import { - GRID_MARGIN_LEFT, - GRID_PADDING_BOTTOM, - GRID_TIME_STEP, - GRID_Y_START, -} from "@web/views/Week/layout.constants"; -import { Columns } from "../Columns/styled"; - -const gridHeight = `100% - (${GRID_Y_START}px + ${GRID_PADDING_BOTTOM}px)`; -const gridRowHeight = `(${gridHeight}) / 11`; -const interval = 60 / GRID_TIME_STEP; -const allDayRowHeight = `${gridRowHeight} / ${interval}`; - -export const StyledAllDayColumns = styled(Columns)` - height: 100%; - - &::before { - background: ${({ theme }) => theme.color.gridLine.primary}; - bottom: 0; - content: ""; - height: 2px; - left: 0; - pointer-events: none; - position: absolute; - right: 0; - } -`; -export const StyledAllDayRow = styled(Flex)<{ rowsCount: number }>` - flex-shrink: 0; - height: ${({ rowsCount }) => - `calc(${allDayRowHeight} * 2 + ${ - rowsCount * 2 || 1 - } * ${allDayRowHeight})`}; - position: relative; - width: 100%; -`; - -export const StyledEvents = styled.div` - position: relative; - height: 100%; - width: 100%; - margin-left: ${GRID_MARGIN_LEFT}px; -`; diff --git a/packages/web/src/views/Week/components/Grid/Columns/styled.ts b/packages/web/src/views/Week/components/Grid/Columns/styled.ts deleted file mode 100644 index 14cacf839..000000000 --- a/packages/web/src/views/Week/components/Grid/Columns/styled.ts +++ /dev/null @@ -1,14 +0,0 @@ -import styled from "styled-components"; -import { - EVENT_WIDTH_MINIMUM, - GRID_MARGIN_LEFT, -} from "@web/views/Week/layout.constants"; - -export const Columns = styled.div` - display: grid; - grid-template-columns: repeat(7, minmax(${EVENT_WIDTH_MINIMUM}px, 1fr)); - left: ${GRID_MARGIN_LEFT}px; - position: absolute; - top: 0; - width: calc(100% - ${GRID_MARGIN_LEFT}px); -`; diff --git a/packages/web/src/views/Week/components/Grid/MainGrid/EdgeNavigationIndicators/EdgeNavigationIndicators.tsx b/packages/web/src/views/Week/components/Grid/MainGrid/EdgeNavigationIndicators/EdgeNavigationIndicators.tsx index 77327ba09..fa728cc87 100644 --- a/packages/web/src/views/Week/components/Grid/MainGrid/EdgeNavigationIndicators/EdgeNavigationIndicators.tsx +++ b/packages/web/src/views/Week/components/Grid/MainGrid/EdgeNavigationIndicators/EdgeNavigationIndicators.tsx @@ -1,6 +1,7 @@ import { type FC } from "react"; +import { type CSSVariables } from "@web/common/styles/css.types"; import { useWeekInteractionEdgeNavigationState } from "@web/views/Week/interaction/state/weekInteractionEdgeNavigationState"; -import { StyledEdgeZone } from "./styled"; +import { GRID_MARGIN_LEFT } from "@web/views/Week/layout.constants"; export const EdgeNavigationIndicators: FC = () => { const dragEdgeState = useWeekInteractionEdgeNavigationState(); @@ -8,5 +9,17 @@ export const EdgeNavigationIndicators: FC = () => { if (!isDragging || !currentEdge) return null; - return ; + return ( +
+ ); }; diff --git a/packages/web/src/views/Week/components/Grid/MainGrid/EdgeNavigationIndicators/styled.ts b/packages/web/src/views/Week/components/Grid/MainGrid/EdgeNavigationIndicators/styled.ts deleted file mode 100644 index 9533b3bf7..000000000 --- a/packages/web/src/views/Week/components/Grid/MainGrid/EdgeNavigationIndicators/styled.ts +++ /dev/null @@ -1,40 +0,0 @@ -import styled, { css } from "styled-components"; -import { ZIndex } from "@web/common/constants/web.constants"; -import { GRID_MARGIN_LEFT } from "@web/views/Week/layout.constants"; - -const BASE_WIDTH = 24; -const MAX_WIDTH = 56; - -export const StyledEdgeZone = styled.div<{ - position: "left" | "right"; - progress: number; -}>` - position: absolute; - top: 0; - bottom: 0; - width: ${({ progress }) => BASE_WIDTH + (MAX_WIDTH - BASE_WIDTH) * (progress / 100)}px; - pointer-events: none; - z-index: ${ZIndex.LAYER_1}; - transition: width 0.05s linear; - ${({ position, progress }) => { - const opacity = 0.04 + (progress / 100) * 0.06; - if (position === "left") { - return css` - left: ${GRID_MARGIN_LEFT}px; - background: linear-gradient( - to right, - rgba(59, 130, 246, ${opacity}), - transparent - ); - `; - } - return css` - right: 0; - background: linear-gradient( - to left, - rgba(59, 130, 246, ${opacity}), - transparent - ); - `; - }} -`; diff --git a/packages/web/src/views/Week/components/Grid/WeekGridScrollArea.tsx b/packages/web/src/views/Week/components/Grid/WeekGridScrollArea.tsx index f0671baf6..de69c8ac8 100644 --- a/packages/web/src/views/Week/components/Grid/WeekGridScrollArea.tsx +++ b/packages/web/src/views/Week/components/Grid/WeekGridScrollArea.tsx @@ -1,17 +1,17 @@ import { type FC, type PropsWithChildren } from "react"; import { ID_WEEK_GRID_SCROLLER } from "@web/common/constants/web.constants"; -import { WeekGridScroller, WeekGridScrollFrame } from "@web/views/Week/styled"; export const WeekGridScrollArea: FC = ({ children }) => { return ( - - +
{children} - - +
+
); }; diff --git a/packages/web/src/views/Week/components/Header/DayLabels.tsx b/packages/web/src/views/Week/components/Header/DayLabels.tsx index d56e9b4f5..71fcb0f27 100644 --- a/packages/web/src/views/Week/components/Header/DayLabels.tsx +++ b/packages/web/src/views/Week/components/Header/DayLabels.tsx @@ -1,10 +1,7 @@ import { type FC } from "react"; -import styled from "styled-components"; import { type Dayjs } from "@core/util/date/dayjs"; -import { theme } from "@web/common/styles/theme"; import { getWeekDayLabel } from "@web/common/utils/event/event.util"; import { Text } from "@web/components/Text/Text"; -import { Columns } from "../Grid/Columns/styled"; interface Props { today: Dayjs; @@ -23,10 +20,10 @@ export const DayLabels: FC = ({ const isCurrentWeek = today.week() === week; const isToday = isCurrentWeek && today.format("DD") === day.format("DD"); const color = day.isBefore(today, "day") - ? theme.color.text.lightInactive + ? "var(--compass-color-text-light-inactive)" : isToday - ? theme.color.text.accent - : theme.color.text.light; + ? "var(--compass-color-accent-primary)" + : "var(--compass-color-text-light)"; return { isToday, color }; }; @@ -43,14 +40,15 @@ export const DayLabels: FC = ({ }; return ( - - +
+
{weekDays.map((day) => { const dayNumber = getDayNumber(day); const { isToday, color } = getColor(day); return ( - = ({ {dayNumber} {day.format("ddd")} - +
); })} - - +
+ ); }; - -const StyledDayLabels = styled.div` - // min-height (not height) so the box grows when clamp() resolves - // .week-day-number to xxl at wide viewports. - min-height: 32px; - margin-top: 10px; - position: relative; - width: 100%; - - .week-day-number { - font-size: ${({ theme }) => - `clamp(${theme.text.size.xl}, 2.7cqw, ${theme.text.size.xxl})`}; - line-height: 1; - } - - .week-day-name { - font-size: ${({ theme }) => - `clamp(${theme.text.size.m}, 2cqw, ${theme.text.size.l})`}; - line-height: 1; - } -`; - -const StyledDayLabelColumns = styled(Columns)` - align-items: end; - height: 100%; -`; - -const StyledDayLabel = styled.div` - align-items: end; - display: flex; - gap: 4px; - justify-content: center; -`; diff --git a/packages/web/src/views/Week/components/Header/Reminder/Reminder.tsx b/packages/web/src/views/Week/components/Header/Reminder/Reminder.tsx index 1cc200015..c369cca58 100644 --- a/packages/web/src/views/Week/components/Header/Reminder/Reminder.tsx +++ b/packages/web/src/views/Week/components/Header/Reminder/Reminder.tsx @@ -11,21 +11,11 @@ import { import { STORAGE_KEYS } from "@web/common/constants/storage.constants"; import { ID_REMINDER_INPUT } from "@web/common/constants/web.constants"; import { useAppHotkey } from "@web/common/hooks/useAppHotkey"; -import { theme } from "@web/common/styles/theme"; import { TooltipWrapper } from "@web/components/Tooltip/TooltipWrapper"; import { selectReminder } from "@web/ducks/events/selectors/view.selectors"; import { viewSlice } from "@web/ducks/events/slices/view.slice"; import { useAppDispatch, useAppSelector } from "@web/store/store.hooks"; import { generateHandDrawnUnderline } from "@web/views/Week/components/Header/Reminder/reminder-util"; -import { - StyledCharCounter, - StyledPlaceholderUnderline, - StyledReminderContainer, - StyledReminderPlaceholder, - StyledReminderText, - StyledReminderWrapper, - StyledUnderline, -} from "./styled"; const MAX_REMINDER_CHARS = 80; @@ -306,27 +296,36 @@ export const Reminder = forwardRef( }; return ( - +
{isEditing ? ( <> - - +
100 + ? "long" + : reminder.length > 50 + ? "medium" + : "short" + } id={ID_REMINDER_INPUT} ref={reminderRef} contentEditable + role="textbox" suppressContentEditableWarning - isEditing={true} - textLength={reminder.length} onBlur={handleReminderBlur} onKeyDown={handleReminderKeyDown} onInput={handleReminderChange} /> - - MAX_REMINDER_CHARS * 0.8} +
+
MAX_REMINDER_CHARS * 0.8} > {reminder.length}/{MAX_REMINDER_CHARS} - +
) : reminder ? ( {}} shortcut="R" > - setShowUnderline(false)} > - 100 + ? "long" + : reminder.length > 50 + ? "medium" + : "short" + } onClick={handleReminderClick} + onKeyDown={(event) => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + handleReminderClick(); + } + }} + role="button" + tabIndex={0} > {reminder} - - + @@ -360,17 +377,17 @@ export const Reminder = forwardRef( > - - + +
) : ( {}} shortcut="R" > - setShowPlaceholderUnderline(false)} > - +
{ + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + handleReminderClick(); + } + }} + role="button" + tabIndex={0} + > Click to add your reminder - - + @@ -400,11 +431,11 @@ export const Reminder = forwardRef( > @@ -412,11 +443,11 @@ export const Reminder = forwardRef( d={placeholderUnderlinePath} stroke="url(#placeholderUnderlineGradient)" /> - - + +
)} -
+ ); }, ); diff --git a/packages/web/src/views/Week/components/Header/Reminder/styled.ts b/packages/web/src/views/Week/components/Header/Reminder/styled.ts deleted file mode 100644 index bd5cd6849..000000000 --- a/packages/web/src/views/Week/components/Header/Reminder/styled.ts +++ /dev/null @@ -1,220 +0,0 @@ -import styled, { css, keyframes } from "styled-components"; -import { ZIndex } from "@web/common/constants/web.constants"; -import { theme } from "@web/common/styles/theme"; - -// Hand-drawn underline animation -const drawHandwrittenUnderline = keyframes` - 0% { - stroke-dashoffset: 1000; - opacity: 0.7; - } - 100% { - stroke-dashoffset: 0; - opacity: 1; - } -`; - -// Animations -const fadeIn = keyframes` - from { - opacity: 0; - transform: translateY(10px); - } - to { - opacity: 1; - transform: translateY(0); - } -`; - -const subtle = keyframes` - 0%, 100% { - filter: brightness(100%); - } - 50% { - filter: brightness(110%); - } -`; - -const gradientWave = keyframes` - 0% { - background-position: 0% 50%; - } - 50% { - background-position: 100% 50%; - } - 100% { - background-position: 0% 50%; - } -`; - -// Character counter -export const StyledCharCounter = styled.div<{ isNearLimit: boolean }>` - position: absolute; - left: 0; - right: 0; - bottom: -2px; - font-size: 12px; - color: ${({ isNearLimit, theme }) => - isNearLimit ? theme.color.status.warning : theme.color.text.lightInactive}; - opacity: 0.8; - text-align: center; - width: 100%; -`; - -export const StyledReminderPlaceholder = styled.div` - font-family: "Caveat", cursive; - font-size: 28px; - color: ${({ theme }) => theme.color.text.lightInactive}; - text-align: center; - font-style: italic; - cursor: pointer; - animation: ${fadeIn} 0.5s ease-out; - position: relative; - padding: 0 10px; - - &:hover { - color: ${({ theme }) => theme.color.text.light}; - transition: color 0.3s ease; - } -`; - -export const StyledPlaceholderUnderline = styled.svg<{ isVisible: boolean }>` - position: absolute; - bottom: 0; - left: -5px; - width: calc(100% + 10px); - height: 20px; - pointer-events: none; - opacity: ${({ isVisible }) => (isVisible ? 1 : 0)}; - - path { - fill: none; - stroke-width: 2.5; - stroke-linecap: round; - stroke-linejoin: round; - stroke-dasharray: 1000; - stroke-dashoffset: ${({ isVisible }) => (isVisible ? 0 : 1000)}; - animation: ${({ isVisible }) => - isVisible ? drawHandwrittenUnderline : "none"} - 0.8s ease-out forwards; - } -`; - -export const StyledReminderContainer = styled.div` - left: 0; - right: 0; - display: flex; - justify-content: center; - align-items: center; - z-index: ${ZIndex.LAYER_1}; - height: 100%; - width: 100%; - margin: 0 auto; -`; - -export const StyledReminderText = styled.div<{ - isEditing: boolean; - textLength: number; -}>` - font-family: "Caveat", cursive; - font-size: ${(props) => { - if (props.textLength > 100) return "22px"; - if (props.textLength > 50) return "24px"; - return "28px"; - }}; - background: ${({ theme }) => - `linear-gradient(90deg, ${theme.color.gradient.accentLight.start}, ${theme.color.gradient.accentLight.end})`}; - background-size: 200% 200%; - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - text-align: center; - font-weight: 600; - filter: ${({ theme }) => `drop-shadow(0 0 2px ${theme.color.bg.primary})`}; - animation: - ${fadeIn} 0.5s ease-out, - ${subtle} 5s ease-in-out infinite, - ${gradientWave} 15s ease infinite; - cursor: ${({ isEditing }) => (isEditing ? "text" : "pointer")}; - min-width: 200px; - max-width: 500px; - position: relative; - - /* Animate drop-shadow color at 50% using bg.secondary */ - @media (prefers-reduced-motion: no-preference) { - & { - animation-name: ${fadeIn}, ${subtle}, ${gradientWave}; - animation-duration: 0.5s, 5s, 15s; - animation-timing-function: ease-out, ease-in-out, ease; - animation-iteration-count: 1, infinite, infinite; - } - /* Use a CSS animation step for 50% */ - @keyframes subtleShadowColor { - 0%, - 100% { - filter: drop-shadow(0 0 2px ${theme.color.bg.primary}); - } - 50% { - filter: drop-shadow(0 0 4px ${theme.color.bg.secondary}); - } - } - animation-name: subtleShadowColor, ${fadeIn}, ${gradientWave}; - animation-duration: 5s, 0.5s, 15s; - animation-timing-function: ease-in-out, ease-out, ease; - animation-iteration-count: infinite, 1, infinite; - } - - ${({ isEditing }) => - isEditing - ? css` - outline: none; - max-height: 80px; - overflow-y: auto; - white-space: pre-wrap; - word-break: break-word; - line-height: 1.3; - padding: 0 10px; - - /* Limit to approximately 3 lines */ - display: -webkit-box; - -webkit-line-clamp: 3; - -webkit-box-orient: vertical; - overflow: hidden; - ` - : css` - white-space: pre-wrap; - word-break: break-word; - line-height: 1.3; - padding: 0 10px; - max-width: 500px; - max-height: 80px; - overflow-y: auto; - `} -`; - -export const StyledReminderWrapper = styled.div` - position: relative; - display: inline-block; - padding-bottom: 18px; /* Add padding to create space for the underline */ -`; - -export const StyledUnderline = styled.svg<{ isVisible: boolean }>` - position: absolute; - bottom: 0; - left: -5px; - width: calc(100% + 10px); - height: 20px; - pointer-events: none; - opacity: ${({ isVisible }) => (isVisible ? 1 : 0)}; - - path { - fill: none; - stroke-width: 3; - stroke-linecap: round; - stroke-linejoin: round; - stroke-dasharray: 1000; - stroke-dashoffset: ${({ isVisible }) => (isVisible ? 0 : 1000)}; - animation: ${({ isVisible }) => - isVisible ? drawHandwrittenUnderline : "none"} - 0.8s ease-out forwards; - } -`; diff --git a/packages/web/src/views/Week/styled.tsx b/packages/web/src/views/Week/styled.tsx deleted file mode 100644 index d4365ac37..000000000 --- a/packages/web/src/views/Week/styled.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { type PropsWithChildren } from "react"; -import styled from "styled-components"; -import { ID_MAIN } from "@web/common/constants/web.constants"; -import { Flex } from "@web/components/Flex/Flex"; -import { WEEK_GRID_TRACK_MIN_WIDTH } from "./layout.constants"; - -export const Styled = styled(Flex)` - height: 100vh; - overflow: hidden; - width: 100vw; -`; - -export function StyledCalendar({ children }: PropsWithChildren) { - return ( -
- {children} -
- ); -} - -export const WeekGridScrollFrame = styled.div` - flex: 1; - min-height: 0; - position: relative; - width: 100%; -`; - -export const WeekGridScroller = styled.div` - height: 100%; - overflow-x: auto; - overflow-y: hidden; - overscroll-behavior-x: contain; - scrollbar-width: none; - width: 100%; - - &::-webkit-scrollbar { - display: none; - height: 0; - width: 0; - } - - &:focus-visible { - outline: ${({ theme }) => `1px solid ${theme.color.text.accent}`}; - outline-offset: -1px; - } -`; - -export const WeekGridTrack = styled.div` - container: week-grid-track / inline-size; - display: flex; - flex-direction: column; - height: 100%; - min-width: ${WEEK_GRID_TRACK_MIN_WIDTH}px; - position: relative; - width: 100%; -`; From 4f154591a97ee52a7ca42dd550a7a8949b8b2a85 Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Thu, 18 Jun 2026 17:30:59 -0500 Subject: [PATCH 08/29] refactor(web): remove styled theme provider --- .../components/CalendarGrid.test.tsx | 62 ++-- .../web/src/common/styles/default-theme.d.ts | 101 ------ .../styles/no-styled-components.test.ts | 20 ++ packages/web/src/common/styles/theme.ts | 3 +- .../CompassProvider/CompassProvider.tsx | 52 ++-- .../ContextMenu/ContextMenuItems.test.tsx | 44 ++- .../PlannerMonthPicker.test.tsx | 12 +- .../SomedayEvent/SomedayEvent.test.tsx | 28 +- .../Calendar/DayCalendarGrid.test.tsx | 6 +- .../Forms/ActionsMenu/ActionsMenu.test.tsx | 9 +- .../RecurrenceSection.test.tsx | 14 +- .../views/Forms/EventForm/EventForm.test.tsx | 288 ++++++++---------- .../Grid/MainGrid/MainGrid.test.tsx | 262 +++++++--------- 13 files changed, 367 insertions(+), 534 deletions(-) delete mode 100644 packages/web/src/common/styles/default-theme.d.ts create mode 100644 packages/web/src/common/styles/no-styled-components.test.ts diff --git a/packages/web/src/common/calendar-grid/components/CalendarGrid.test.tsx b/packages/web/src/common/calendar-grid/components/CalendarGrid.test.tsx index 89997d45c..cc6a9fc3e 100644 --- a/packages/web/src/common/calendar-grid/components/CalendarGrid.test.tsx +++ b/packages/web/src/common/calendar-grid/components/CalendarGrid.test.tsx @@ -1,8 +1,6 @@ import { cleanup, render, screen, within } from "@testing-library/react"; import { type RefCallback } from "react"; -import { ThemeProvider } from "styled-components"; import dayjs from "@core/util/date/dayjs"; -import { theme } from "@web/common/styles/theme"; import { afterEach, describe, expect, it, mock } from "bun:test"; import "@testing-library/jest-dom"; import { CalendarGrid } from "./CalendarGrid"; @@ -19,20 +17,18 @@ const createGridRefs = () => ({ const renderGrid = (count: number) => render( - - } - gridRefs={createGridRefs()} - onAllDayMouseDown={mock()} - onTimedMouseDown={mock()} - timedEventsLayer={
} - today={dayjs("2026-05-20T00:00:00.000")} - visibleDates={Array.from({ length: count }, (_, index) => ({ - date: dayjs("2026-05-18T00:00:00.000").add(index, "day"), - key: `date-${index}`, - }))} - /> - , + } + gridRefs={createGridRefs()} + onAllDayMouseDown={mock()} + onTimedMouseDown={mock()} + timedEventsLayer={
} + today={dayjs("2026-05-20T00:00:00.000")} + visibleDates={Array.from({ length: count }, (_, index) => ({ + date: dayjs("2026-05-18T00:00:00.000").add(index, "day"), + key: `date-${index}`, + }))} + />, ); afterEach(() => { @@ -67,24 +63,22 @@ describe("CalendarGrid", () => { it("passes the all-day row count to the shared all-day surface", () => { render( - - } - allDayGridOffsetTopPx={123} - allDayRowsCount={3} - gridRefs={createGridRefs()} - onAllDayMouseDown={mock()} - onTimedMouseDown={mock()} - timedEventsLayer={
} - today={dayjs("2026-05-20T00:00:00.000")} - visibleDates={[ - { - date: dayjs("2026-05-20T00:00:00.000"), - key: "date-0", - }, - ]} - /> - , + } + allDayGridOffsetTopPx={123} + allDayRowsCount={3} + gridRefs={createGridRefs()} + onAllDayMouseDown={mock()} + onTimedMouseDown={mock()} + timedEventsLayer={
} + today={dayjs("2026-05-20T00:00:00.000")} + visibleDates={[ + { + date: dayjs("2026-05-20T00:00:00.000"), + key: "date-0", + }, + ]} + />, ); expect( diff --git a/packages/web/src/common/styles/default-theme.d.ts b/packages/web/src/common/styles/default-theme.d.ts deleted file mode 100644 index 1bd5fd12b..000000000 --- a/packages/web/src/common/styles/default-theme.d.ts +++ /dev/null @@ -1,101 +0,0 @@ -import "styled-components"; -import { type textLight } from "./colors"; - -declare module "styled-components" { - export interface DefaultTheme { - color: { - common: { - white: string; - black: string; - }; - bg: { - primary: string; - secondary: string; - }; - border: { - primary: string; - primaryDark: string; - secondary: string; - }; - fg: { - primary: string; - primaryDark: string; - }; - gradient: { - accentLight: { - start: string; - end: string; - }; - }; - gridLine: { - primary: string; - }; - menu: { - bg: string; - }; - panel: { - bg: string; - scrollbar: string; - scrollbarActive: string; - shadow: string; - text: string; - }; - shadow: { - default: string; - }; - status: { - success: string; - error: string; - warning: string; - info: string; - }; - tag: { - one: string; - two: string; - three: string; - }; - text: { - accent: string; - light: string; - lighter: string; - lightInactive: string; - dark: string; - darkPlaceholder: string; - }; - }; - text: { - size: { - xs: string; - s: string; - m: string; - l: string; - xl: string; - xxl: string; - xxxl: string; - "4xl": string; - "5xl": string; - }; - weight: { - light: number; - regular: number; - medium: number; - bold: number; - extraBold: number; - }; - }; - getContrastText: (backgroundColor: string) => typeof textLight; - transition: { - default: string; - }; - shape: { - borderRadius: string; - }; - spacing: { - xs: string; - s: string; - m: string; - l: string; - xl: string; - }; - } -} diff --git a/packages/web/src/common/styles/no-styled-components.test.ts b/packages/web/src/common/styles/no-styled-components.test.ts new file mode 100644 index 000000000..178fad468 --- /dev/null +++ b/packages/web/src/common/styles/no-styled-components.test.ts @@ -0,0 +1,20 @@ +import { expect, test } from "bun:test"; +import { readdirSync, readFileSync, statSync } from "node:fs"; +import { join } from "node:path"; + +const srcRoot = join(import.meta.dir, "../.."); +const files = (directory: string): string[] => + readdirSync(directory).flatMap((name) => { + const path = join(directory, name); + return statSync(path).isDirectory() ? files(path) : [path]; + }); + +test("web source has no styled-components imports", () => { + const packageName = ["styled", "components"].join("-"); + const offenders = files(srcRoot).filter( + (file) => + /\.[jt]sx?$/.test(file) && + readFileSync(file, "utf8").includes(`from "${packageName}"`), + ); + expect(offenders).toEqual([]); +}); diff --git a/packages/web/src/common/styles/theme.ts b/packages/web/src/common/styles/theme.ts index 13af7d2de..9e4488fc8 100644 --- a/packages/web/src/common/styles/theme.ts +++ b/packages/web/src/common/styles/theme.ts @@ -1,7 +1,6 @@ -import { type DefaultTheme } from "styled-components"; import { c, textDark, textLight } from "@web/common/styles/colors"; -export const theme: DefaultTheme = { +export const theme = { color: { common: { white: c.white100, diff --git a/packages/web/src/components/CompassProvider/CompassProvider.tsx b/packages/web/src/components/CompassProvider/CompassProvider.tsx index 5294c0107..38c5b7c0c 100644 --- a/packages/web/src/components/CompassProvider/CompassProvider.tsx +++ b/packages/web/src/components/CompassProvider/CompassProvider.tsx @@ -3,7 +3,6 @@ import { PostHogProvider } from "@web/auth/posthog/posthog-react"; import { type PropsWithChildren } from "react"; import { Provider } from "react-redux"; import { ToastContainer } from "react-toastify"; -import { ThemeProvider } from "styled-components"; import { GoogleOAuthProvider } from "@react-oauth/google"; import { HotkeysProvider } from "@tanstack/react-hotkeys"; import { SessionProvider } from "@web/auth/compass/session/SessionProvider"; @@ -11,7 +10,6 @@ import { isPosthogEnabled } from "@web/auth/posthog/posthog.util"; import { ENV_WEB } from "@web/common/constants/env.constants"; import { CompassRefsProvider } from "@web/common/context/compass-refs"; import { PointerPositionProvider } from "@web/common/context/pointer-position"; -import { theme } from "@web/common/styles/theme"; import { AuthModal } from "@web/components/AuthModal/AuthModal"; import { AuthModalProvider } from "@web/components/AuthModal/AuthModalProvider"; import { WelcomeModal } from "@web/components/WelcomeModal/WelcomeModal"; @@ -39,32 +37,30 @@ export const CompassRequiredProviders = ( - - - - - - {props.children} - - - - - - - - + + + + + {props.children} + + + + + + + diff --git a/packages/web/src/components/ContextMenu/ContextMenuItems.test.tsx b/packages/web/src/components/ContextMenu/ContextMenuItems.test.tsx index 26b2201a0..90e3de214 100644 --- a/packages/web/src/components/ContextMenu/ContextMenuItems.test.tsx +++ b/packages/web/src/components/ContextMenu/ContextMenuItems.test.tsx @@ -3,13 +3,11 @@ import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { type ReactElement } from "react"; import { Provider } from "react-redux"; -import { ThemeProvider } from "styled-components"; import { createMockStandaloneEvent } from "@core/util/test/ccal.event.factory"; import { createInitialState, type InitialReduxState, } from "@web/__tests__/utils/state/store.test.util"; -import { theme } from "@web/common/styles/theme"; import { type Schema_GridEvent } from "@web/common/types/web.event.types"; import { gridEventDefaultPosition } from "@web/common/utils/event/event.util"; import { reducers } from "@web/store/reducers"; @@ -76,28 +74,26 @@ const renderWithTheme = (ui: ReactElement) => { return render( - - false), - submit: mockSubmit, - }, - confirmation: { - onDelete: mockOnDelete, - }, - setters: { - setDraft: mockSetDraft, - }, - } as never - } - > - {ui} - - + false), + submit: mockSubmit, + }, + confirmation: { + onDelete: mockOnDelete, + }, + setters: { + setDraft: mockSetDraft, + }, + } as never + } + > + {ui} + , ); }; diff --git a/packages/web/src/components/PlannerSidebar/PlannerMonthPicker/PlannerMonthPicker.test.tsx b/packages/web/src/components/PlannerSidebar/PlannerMonthPicker/PlannerMonthPicker.test.tsx index 767895587..780e76fa2 100644 --- a/packages/web/src/components/PlannerSidebar/PlannerMonthPicker/PlannerMonthPicker.test.tsx +++ b/packages/web/src/components/PlannerSidebar/PlannerMonthPicker/PlannerMonthPicker.test.tsx @@ -1,8 +1,6 @@ import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import { ThemeProvider } from "styled-components"; import dayjs from "@core/util/date/dayjs"; -import { theme } from "@web/common/styles/theme"; import { PlannerMonthPicker } from "@web/components/PlannerSidebar/PlannerMonthPicker/PlannerMonthPicker"; import { describe, expect, it, mock } from "bun:test"; @@ -15,12 +13,10 @@ describe("PlannerMonthPicker", () => { const onSelectDate = mock(); render( - - - , + , ); await user.click(screen.getByLabelText("Choose Monday, May 25th, 2026")); diff --git a/packages/web/src/components/PlannerSidebar/SomedayEventSections/SomedayEvents/SomedayEvent/SomedayEvent.test.tsx b/packages/web/src/components/PlannerSidebar/SomedayEventSections/SomedayEvents/SomedayEvent/SomedayEvent.test.tsx index 744226ae9..f0a27ce41 100644 --- a/packages/web/src/components/PlannerSidebar/SomedayEventSections/SomedayEvents/SomedayEvent/SomedayEvent.test.tsx +++ b/packages/web/src/components/PlannerSidebar/SomedayEventSections/SomedayEvents/SomedayEvent/SomedayEvent.test.tsx @@ -1,8 +1,6 @@ import { fireEvent, render, screen } from "@testing-library/react"; -import { ThemeProvider } from "styled-components"; import { Priorities } from "@core/constants/core.constants"; import { Categories_Event, type Schema_Event } from "@core/types/event.types"; -import { theme } from "@web/common/styles/theme"; import { type Props_DraftForm } from "@web/views/Week/components/Draft/hooks/state/useDraftForm"; import { SomedayEvent } from "./SomedayEvent"; import { describe, expect, it, mock } from "bun:test"; @@ -27,20 +25,18 @@ const renderSomedayEvent = ({ onClick?: () => void; } = {}) => { render( - - undefined} - onBlur={() => undefined} - onClick={onClick} - onFocus={() => undefined} - onMigrate={() => undefined} - priority={Priorities.WORK} - status={{ isDrafting: false, isDragging: false }} - /> - , + undefined} + onBlur={() => undefined} + onClick={onClick} + onFocus={() => undefined} + onMigrate={() => undefined} + priority={Priorities.WORK} + status={{ isDrafting: false, isDragging: false }} + />, ); return { onClick }; diff --git a/packages/web/src/views/Day/components/Calendar/DayCalendarGrid.test.tsx b/packages/web/src/views/Day/components/Calendar/DayCalendarGrid.test.tsx index cd7720b37..daa3dd6e5 100644 --- a/packages/web/src/views/Day/components/Calendar/DayCalendarGrid.test.tsx +++ b/packages/web/src/views/Day/components/Calendar/DayCalendarGrid.test.tsx @@ -7,14 +7,12 @@ import { within, } from "@testing-library/react"; import { Provider } from "react-redux"; -import { ThemeProvider } from "styled-components"; import { type Schema_Event } from "@core/types/event.types"; import dayjs from "@core/util/date/dayjs"; import { createStoreWithEvents } from "@web/__tests__/utils/state/store.test.util"; import { CALENDAR_TIMED_EVENT_FAN_INDENT } from "@web/common/calendar-grid/calendarGrid.constants"; import { type CalendarGridMeasurements } from "@web/common/calendar-grid/types/calendarGrid.types"; import { ZIndex } from "@web/common/constants/web.constants"; -import { theme } from "@web/common/styles/theme"; import { CompassDOMEvents, compassEventEmitter, @@ -101,9 +99,7 @@ const { DayCalendarGrid } = const renderDayCalendarGrid = () => render( - - - + , ); diff --git a/packages/web/src/views/Forms/ActionsMenu/ActionsMenu.test.tsx b/packages/web/src/views/Forms/ActionsMenu/ActionsMenu.test.tsx index 830c349e6..c42c87a3e 100644 --- a/packages/web/src/views/Forms/ActionsMenu/ActionsMenu.test.tsx +++ b/packages/web/src/views/Forms/ActionsMenu/ActionsMenu.test.tsx @@ -1,12 +1,9 @@ import { fireEvent, render, screen } from "@testing-library/react"; import { type ReactElement } from "react"; -import { ThemeProvider } from "styled-components"; -import { theme } from "@web/common/styles/theme"; import { ActionsMenu, useMenuContext } from "./ActionsMenu"; import { describe, expect, it } from "bun:test"; -const renderWithTheme = (ui: ReactElement) => - render({ui}); +const renderMenu = (ui: ReactElement) => render(ui); const TestMenuItem = () => { const menuContext = useMenuContext(); @@ -21,7 +18,7 @@ const TestMenuItem = () => { describe("ActionsMenu", () => { it("keeps mouse hover from stealing focus from the editor action trigger", () => { - renderWithTheme( + renderMenu( {() => }, ); @@ -35,7 +32,7 @@ describe("ActionsMenu", () => { }); it("keeps the menu mounted when focus moves inside it", () => { - renderWithTheme( + renderMenu( {() => }, ); diff --git a/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/RecurrenceSection.test.tsx b/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/RecurrenceSection.test.tsx index d7f59a3a2..919a053ad 100644 --- a/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/RecurrenceSection.test.tsx +++ b/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/RecurrenceSection.test.tsx @@ -1,10 +1,8 @@ import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { useCallback, useState } from "react"; -import { ThemeProvider } from "styled-components"; import { Origin, Priorities } from "@core/constants/core.constants"; import { type Schema_Event } from "@core/types/event.types"; -import { theme } from "@web/common/styles/theme"; import { type Schema_GridEvent } from "@web/common/types/web.event.types"; import { assembleGridEvent } from "@web/common/utils/event/event.util"; import { createRecurrenceSection } from "./RecurrenceSection"; @@ -47,13 +45,11 @@ function renderRecurrenceSection({ if (!event) return null; return ( - - - + ); } diff --git a/packages/web/src/views/Forms/EventForm/EventForm.test.tsx b/packages/web/src/views/Forms/EventForm/EventForm.test.tsx index 87cab4246..5227885f1 100644 --- a/packages/web/src/views/Forms/EventForm/EventForm.test.tsx +++ b/packages/web/src/views/Forms/EventForm/EventForm.test.tsx @@ -2,11 +2,9 @@ import { HotkeyManager, resolveModifier } from "@tanstack/react-hotkeys"; import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { createRef, type SetStateAction, useState } from "react"; -import { ThemeProvider } from "styled-components"; import { Origin, Priorities } from "@core/constants/core.constants"; import { type Schema_Event } from "@core/types/event.types"; import dayjs from "@core/util/date/dayjs"; -import { theme } from "@web/common/styles/theme"; import { type Props as DateTimeSectionProps } from "@web/views/Forms/EventForm/DateControlsSection/DateTimeSection/DateTimeSection"; import { getFormDates } from "@web/views/Forms/EventForm/DateControlsSection/DateTimeSection/form.datetime.util"; import { beforeEach, describe, expect, it, mock } from "bun:test"; @@ -104,18 +102,16 @@ describe("EventForm", () => { const onDuplicate = mock(); render( - - - , + , ); const titleField = screen.getByPlaceholderText("Title"); @@ -135,19 +131,17 @@ describe("EventForm", () => { const onDraftTitleArrowKey = mock(() => true); render( - - - , + , ); const titleField = screen.getByPlaceholderText("Title"); @@ -171,20 +165,18 @@ describe("EventForm", () => { const onDraftTitleArrowKey = mock(() => true); const { rerender } = render( - - - , + , ); const titleField = screen.getByPlaceholderText("Title"); @@ -194,20 +186,18 @@ describe("EventForm", () => { expect(onDraftTitleArrowKey).not.toHaveBeenCalled(); rerender( - - - , + , ); await waitFor(() => { @@ -225,33 +215,29 @@ describe("EventForm", () => { }; const { rerender } = render( - - - , + , ); rerender( - - - , + , ); const expected = getFormDates(nextEvent.startDate, nextEvent.endDate); @@ -271,19 +257,17 @@ describe("EventForm", () => { const onDraftTitleArrowKey = mock(() => true); render( - - - , + , ); const titleField = screen.getByPlaceholderText("Title"); @@ -298,19 +282,17 @@ describe("EventForm", () => { const onDraftTitleArrowKey = mock(() => true); render( - - - , + , ); const titleField = screen.getByPlaceholderText("Title"); @@ -325,19 +307,17 @@ describe("EventForm", () => { const onDraftTitleArrowKey = mock(() => true); render( - - - , + , ); const titleField = screen.getByPlaceholderText("Title"); @@ -370,18 +350,16 @@ describe("EventForm", () => { }; return ( - - - + ); } @@ -404,18 +382,16 @@ describe("EventForm", () => { const onSubmit = mock(); render( - - - , + , ); const titleField = screen.getByPlaceholderText("Title"); @@ -431,7 +407,7 @@ describe("EventForm", () => { const onSubmit = mock(); render( - + <> { onSubmit={onSubmit} setEvent={mock()} /> - , + , ); await user.click(screen.getByRole("button", { name: "Draft block" })); @@ -457,19 +433,17 @@ describe("EventForm", () => { const titleInputRef = createRef(); render( - - - , + , ); expect(titleInputRef.current).toBe(screen.getByPlaceholderText("Title")); diff --git a/packages/web/src/views/Week/components/Grid/MainGrid/MainGrid.test.tsx b/packages/web/src/views/Week/components/Grid/MainGrid/MainGrid.test.tsx index 3b5a4d07f..17da7c388 100644 --- a/packages/web/src/views/Week/components/Grid/MainGrid/MainGrid.test.tsx +++ b/packages/web/src/views/Week/components/Grid/MainGrid/MainGrid.test.tsx @@ -7,7 +7,6 @@ import { waitFor, } from "@testing-library/react"; import { Provider } from "react-redux"; -import { ThemeProvider } from "styled-components"; import { type Schema_Event } from "@core/types/event.types"; import dayjs, { type Dayjs } from "@core/util/date/dayjs"; import { createInitialState } from "@web/__tests__/utils/state/store.test.util"; @@ -15,7 +14,6 @@ import { ID_GRID_COLUMNS_TIMED, ZIndex, } from "@web/common/constants/web.constants"; -import { theme } from "@web/common/styles/theme"; import { pendingEventsSlice } from "@web/ducks/events/slices/pending.slice"; import { reducers } from "@web/store/reducers"; import { DraftContext } from "@web/views/Week/components/Draft/context/DraftContext"; @@ -155,28 +153,26 @@ const renderMainGrid = () => { const view = render( - - - - - + + + , ); @@ -190,38 +186,36 @@ const renderGridRegions = () => { return render( - - - - - - + + + + , ); }; @@ -232,37 +226,35 @@ const renderWeekGrid = (events: Schema_Event[] = []) => { return render( - - - - - + + + , ); }; @@ -422,12 +414,10 @@ describe("Week calendar accessibility", () => { render( - - - + , ); @@ -448,12 +438,10 @@ describe("Week calendar accessibility", () => { render( - - - + , ); @@ -474,12 +462,10 @@ describe("Week calendar accessibility", () => { render( - - - + , ); @@ -509,13 +495,11 @@ describe("Week calendar accessibility", () => { render( - - - + , ); @@ -544,13 +528,11 @@ describe("Week calendar accessibility", () => { render( - - - + , ); @@ -611,12 +593,10 @@ describe("saved Week event ownership", () => { render( - - - + , ); @@ -655,12 +635,10 @@ describe("saved Week event ownership", () => { render( - - - + , ); @@ -681,12 +659,10 @@ describe("saved Week event ownership", () => { render( - - - + , ); @@ -704,13 +680,11 @@ describe("saved Week event ownership", () => { render( - - - + , ); From 809861e92d8e4e56b699bd9c9db06bd9b25cf7b4 Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Thu, 18 Jun 2026 17:32:09 -0500 Subject: [PATCH 09/29] refactor(web): remove styled components dependency --- CONTEXT.md | 2 +- babel.config.js | 9 ---- bun.lock | 44 ++----------------- docs/frontend/frontend-runtime-flow.md | 4 +- packages/web/package.json | 2 - .../styles/no-styled-components.test.ts | 18 +++++++- 6 files changed, 23 insertions(+), 56 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index aa2ef27bd..32bf0c8d1 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -271,7 +271,7 @@ during Import or Public watch notification handling. - Treat recurring event changes as changes to a **Recurring Series**, not just isolated event rows. - Do not use **Someday Event** and **Task** interchangeably. -- Do not describe the frontend as Tailwind-only or styled-components-only. +- Frontend styling uses Tailwind utilities and semantic runtime CSS variables. - Do not describe local self-hosting as proving continuous Google sync unless a public HTTPS webhook path has been configured and verified. - When changing a shared event, sync, API, or error contract, keep the web, diff --git a/babel.config.js b/babel.config.js index 5d94edff8..c950bb763 100644 --- a/babel.config.js +++ b/babel.config.js @@ -4,13 +4,4 @@ module.exports = { ["@babel/preset-env", { targets: { node: "current" } }], "@babel/preset-typescript", ], - plugins: [ - [ - "babel-plugin-styled-components", - { - meaninglessFileNames: ["index", "styled"], - pure: true, - }, - ], - ], }; diff --git a/bun.lock b/bun.lock index ac6a7d172..816b22ee4 100644 --- a/bun.lock +++ b/bun.lock @@ -120,7 +120,6 @@ "redux": "^4.1.1", "redux-saga": "^1.1.3", "rxjs": "^7.8.0", - "styled-components": "^5.3.1", "supertokens-web-js": "^0.16.0", }, "devDependencies": { @@ -133,7 +132,6 @@ "@types/react": "^18.0.8", "@types/react-datepicker": "^4.1.7", "@types/react-dom": "^18.0.3", - "@types/styled-components": "^5.1.14", "bun-types": "^1.2.18", "fake-indexeddb": "^6.2.5", "jsdom": "^26.1.0", @@ -441,8 +439,6 @@ "@emotion/hash": ["@emotion/hash@0.9.2", "", {}, "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g=="], - "@emotion/is-prop-valid": ["@emotion/is-prop-valid@1.4.0", "", { "dependencies": { "@emotion/memoize": "^0.9.0" } }, "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw=="], - "@emotion/memoize": ["@emotion/memoize@0.9.0", "", {}, "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ=="], "@emotion/react": ["@emotion/react@11.14.0", "", { "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", "@emotion/cache": "^11.14.0", "@emotion/serialize": "^1.3.3", "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", "@emotion/utils": "^1.4.2", "@emotion/weak-memoize": "^0.4.0", "hoist-non-react-statics": "^3.3.1" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA=="], @@ -451,9 +447,7 @@ "@emotion/sheet": ["@emotion/sheet@1.4.0", "", {}, "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg=="], - "@emotion/stylis": ["@emotion/stylis@0.8.5", "", {}, "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ=="], - - "@emotion/unitless": ["@emotion/unitless@0.7.5", "", {}, "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg=="], + "@emotion/unitless": ["@emotion/unitless@0.10.0", "", {}, "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg=="], "@emotion/use-insertion-effect-with-fallbacks": ["@emotion/use-insertion-effect-with-fallbacks@1.2.0", "", { "peerDependencies": { "react": ">=16.8.0" } }, "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg=="], @@ -829,8 +823,6 @@ "@types/stack-utils": ["@types/stack-utils@2.0.3", "", {}, "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw=="], - "@types/styled-components": ["@types/styled-components@5.1.36", "", { "dependencies": { "@types/hoist-non-react-statics": "*", "@types/react": "*", "csstype": "^3.2.2" } }, "sha512-pGMRNY5G2rNDKEv2DOiFYa7Ft1r0jrhmgBwHhOMzPTgCjO76bCot0/4uEfqj7K0Jf1KdQmDtAuaDk9EAs9foSw=="], - "@types/superagent": ["@types/superagent@8.1.9", "", { "dependencies": { "@types/cookiejar": "^2.1.5", "@types/methods": "^1.1.4", "@types/node": "*", "form-data": "^4.0.0" } }, "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ=="], "@types/supertest": ["@types/supertest@6.0.3", "", { "dependencies": { "@types/methods": "^1.1.4", "@types/superagent": "^8.1.0" } }, "sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w=="], @@ -915,8 +907,6 @@ "babel-plugin-polyfill-regenerator": ["babel-plugin-polyfill-regenerator@0.6.8", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.8" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg=="], - "babel-plugin-styled-components": ["babel-plugin-styled-components@2.1.4", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-module-imports": "^7.22.5", "@babel/plugin-syntax-jsx": "^7.22.5", "lodash": "^4.17.21", "picomatch": "^2.3.1" }, "peerDependencies": { "styled-components": ">= 2" } }, "sha512-Xgp9g+A/cG47sUyRwwYxGM4bR/jDRg5N6it/8+HxCnbT5XNKSKDT9xm4oag/osgqjC2It/vH0yXsomOG6k558g=="], - "babel-preset-current-node-syntax": ["babel-preset-current-node-syntax@1.2.0", "", { "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-bigint": "^7.8.3", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-import-attributes": "^7.24.7", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-numeric-separator": "^7.10.4", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0 || ^8.0.0-0" } }, "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg=="], "babel-preset-jest": ["babel-preset-jest@29.6.3", "", { "dependencies": { "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA=="], @@ -987,8 +977,6 @@ "camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="], - "camelize": ["camelize@1.0.1", "", {}, "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ=="], - "caniuse-lite": ["caniuse-lite@1.0.30001787", "", {}, "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg=="], "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -1073,12 +1061,8 @@ "crypto-js": ["crypto-js@4.2.0", "", {}, "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="], - "css-color-keywords": ["css-color-keywords@1.0.0", "", {}, "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg=="], - "css-select": ["css-select@4.3.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.0.1", "domhandler": "^4.3.1", "domutils": "^2.8.0", "nth-check": "^2.0.1" } }, "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ=="], - "css-to-react-native": ["css-to-react-native@3.2.0", "", { "dependencies": { "camelize": "^1.0.0", "css-color-keywords": "^1.0.0", "postcss-value-parser": "^4.0.2" } }, "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ=="], - "css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="], "css.escape": ["css.escape@1.5.1", "", {}, "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="], @@ -1319,7 +1303,7 @@ "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], - "has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], @@ -1977,8 +1961,6 @@ "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], - "shallowequal": ["shallowequal@1.1.0", "", {}, "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="], - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], @@ -2041,8 +2023,6 @@ "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], - "styled-components": ["styled-components@5.3.11", "", { "dependencies": { "@babel/helper-module-imports": "^7.0.0", "@babel/traverse": "^7.4.5", "@emotion/is-prop-valid": "^1.1.0", "@emotion/stylis": "^0.8.4", "@emotion/unitless": "^0.7.4", "babel-plugin-styled-components": ">= 1.12.0", "css-to-react-native": "^3.0.0", "hoist-non-react-statics": "^3.0.0", "shallowequal": "^1.1.0", "supports-color": "^5.5.0" }, "peerDependencies": { "react": ">= 16.8.0", "react-dom": ">= 16.8.0", "react-is": ">= 16.8.0" } }, "sha512-uuzIIfnVkagcVHv9nE0VPlHPSCmXIUGKfJ42LNjxCCTDTL5sgnJ8Z7GZBq0EnLYGln77tPpEpExt2+qa+cZqSw=="], - "stylis": ["stylis@4.2.0", "", {}, "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="], "stylus": ["stylus@0.62.0", "", { "dependencies": { "@adobe/css-tools": "~4.3.1", "debug": "^4.3.2", "glob": "^7.1.6", "sax": "~1.3.0", "source-map": "^0.7.3" }, "bin": { "stylus": "bin/stylus" } }, "sha512-v3YCf31atbwJQIMtPNX8hcQ+okD4NQaTuKGUWfII8eaqn+3otrbttGL1zSMZAAtiPsBztQnujVBugg/cXFUpyg=="], @@ -2059,7 +2039,7 @@ "supertokens-website": ["supertokens-website@20.1.6", "", { "dependencies": { "browser-tabs-lock": "^1.3.0", "supertokens-js-override": "^0.0.4" } }, "sha512-WSehco2PsrFp4WY7h6tDutYyi2nPgJS8lahUadcL/cpBqgEuZ3pjnvN1NDASSNSTfXeO1smm3HrvvLZG5C7qHA=="], - "supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], @@ -2245,8 +2225,6 @@ "@emotion/babel-plugin/source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="], - "@emotion/serialize/@emotion/unitless": ["@emotion/unitless@0.10.0", "", {}, "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg=="], - "@inquirer/core/cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], "@inquirer/core/mute-stream": ["mute-stream@3.0.0", "", {}, "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw=="], @@ -2345,8 +2323,6 @@ "chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "clean-css/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], @@ -2383,8 +2359,6 @@ "istanbul-lib-report/make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="], - "istanbul-lib-report/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "istanbul-lib-source-maps/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "jest-circus/@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], @@ -2489,8 +2463,6 @@ "@rushstack/node-core-library/semver/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], - "@rushstack/terminal/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - "@shelf/jest-mongodb/debug/ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="], "@shelf/jest-mongodb/mongodb/bson": ["bson@6.10.4", "", {}, "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng=="], @@ -2517,8 +2489,6 @@ "@testing-library/jest-dom/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "@testing-library/jest-dom/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "@types/body-parser/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], "@types/connect/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], @@ -2545,8 +2515,6 @@ "bun-types/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - "chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "color/color-convert/color-name": ["color-name@2.1.0", "", {}, "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg=="], @@ -2557,8 +2525,6 @@ "istanbul-lib-report/make-dir/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - "istanbul-lib-report/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - "jest-circus/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], "jest-environment-node/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], @@ -2577,8 +2543,6 @@ "jest-worker/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - "jest-worker/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - "mongodb-memory-server-core/mongodb/bson": ["bson@5.5.1", "", {}, "sha512-ix0EwukN2EpC0SRWIj/7B5+A6uQMQy6KMREI9qQqvgpkV2frH63T0UDVd1SYedL6dNCmDBYB3QtXi4ISk9YT+g=="], "mongodb-memory-server-core/mongodb/mongodb-connection-string-url": ["mongodb-connection-string-url@2.6.0", "", { "dependencies": { "@types/whatwg-url": "^8.2.1", "whatwg-url": "^11.0.0" } }, "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ=="], @@ -2613,8 +2577,6 @@ "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@testing-library/jest-dom/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - "mongodb-memory-server-core/mongodb/mongodb-connection-string-url/@types/whatwg-url": ["@types/whatwg-url@8.2.2", "", { "dependencies": { "@types/node": "*", "@types/webidl-conversions": "*" } }, "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA=="], "mongodb-memory-server-core/mongodb/mongodb-connection-string-url/whatwg-url": ["whatwg-url@11.0.0", "", { "dependencies": { "tr46": "^3.0.0", "webidl-conversions": "^7.0.0" } }, "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ=="], diff --git a/docs/frontend/frontend-runtime-flow.md b/docs/frontend/frontend-runtime-flow.md index 8a9fdfdf1..6230d9a47 100644 --- a/docs/frontend/frontend-runtime-flow.md +++ b/docs/frontend/frontend-runtime-flow.md @@ -214,10 +214,10 @@ Important consequence: The web app currently uses two styling systems in parallel: -- longstanding `styled-components` for much of the existing UI +- Tailwind utilities for component styling - Tailwind v4 utilities and semantic theme tokens from `packages/web/src/index.css` for newer or migrated surfaces -Do not describe the frontend as Tailwind-only or styled-components-only. Follow the local pattern of the area you are editing unless the change is explicitly migrating that area. +Use the existing `c-*` component utility convention and semantic colors from `packages/web/src/index.css`. Runtime theme values belong in `--compass-*` CSS variables so alternate themes can override values without rebuilding component styles. ## Day Task Drag Handle Positioning diff --git a/packages/web/package.json b/packages/web/package.json index 509fc2634..58a2b3f13 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -34,7 +34,6 @@ "redux": "^4.1.1", "redux-saga": "^1.1.3", "rxjs": "^7.8.0", - "styled-components": "^5.3.1", "supertokens-web-js": "^0.16.0" }, "devDependencies": { @@ -47,7 +46,6 @@ "@types/react": "^18.0.8", "@types/react-datepicker": "^4.1.7", "@types/react-dom": "^18.0.3", - "@types/styled-components": "^5.1.14", "bun-types": "^1.2.18", "fake-indexeddb": "^6.2.5", "jsdom": "^26.1.0", diff --git a/packages/web/src/common/styles/no-styled-components.test.ts b/packages/web/src/common/styles/no-styled-components.test.ts index 178fad468..d75ad04e3 100644 --- a/packages/web/src/common/styles/no-styled-components.test.ts +++ b/packages/web/src/common/styles/no-styled-components.test.ts @@ -1,6 +1,6 @@ import { expect, test } from "bun:test"; import { readdirSync, readFileSync, statSync } from "node:fs"; -import { join } from "node:path"; +import { join, resolve } from "node:path"; const srcRoot = join(import.meta.dir, "../.."); const files = (directory: string): string[] => @@ -18,3 +18,19 @@ test("web source has no styled-components imports", () => { ); expect(offenders).toEqual([]); }); + +test("the removed styling package and Babel plugin stay absent", () => { + const repositoryRoot = resolve(srcRoot, "../../.."); + const manifest = readFileSync( + join(repositoryRoot, "packages/web/package.json"), + "utf8", + ); + const babelConfig = readFileSync( + join(repositoryRoot, "babel.config.js"), + "utf8", + ); + const packageName = ["styled", "components"].join("-"); + + expect(manifest).not.toContain(packageName); + expect(babelConfig).not.toContain(`babel-plugin-${packageName}`); +}); From 59091ce466fc56c5ebac2b7a1dc1250b3675ab5c Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Thu, 18 Jun 2026 18:27:26 -0500 Subject: [PATCH 10/29] fix(web): restore role=form on event forms The styled-to-tailwind migration replaced with a bare
, dropping the explicit ARIA role. A without an accessible name exposes no "form" role, so getByRole("form") stopped matching and broke the event-form e2e flows (19 specs). Restore role="form" on both the timed and someday event forms. Co-Authored-By: Claude Opus 4.8 --- packages/web/src/views/Forms/EventForm/EventForm.tsx | 1 + .../web/src/views/Forms/SomedayEventForm/SomedayEventForm.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/web/src/views/Forms/EventForm/EventForm.tsx b/packages/web/src/views/Forms/EventForm/EventForm.tsx index 5091abd82..44f525f9f 100644 --- a/packages/web/src/views/Forms/EventForm/EventForm.tsx +++ b/packages/web/src/views/Forms/EventForm/EventForm.tsx @@ -455,6 +455,7 @@ export const EventForm: React.FC> = memo( return ( { diff --git a/packages/web/src/views/Forms/SomedayEventForm/SomedayEventForm.tsx b/packages/web/src/views/Forms/SomedayEventForm/SomedayEventForm.tsx index 3a9b0d7f8..10d322b06 100644 --- a/packages/web/src/views/Forms/SomedayEventForm/SomedayEventForm.tsx +++ b/packages/web/src/views/Forms/SomedayEventForm/SomedayEventForm.tsx @@ -161,6 +161,7 @@ export const SomedayEventForm: React.FC = ({ return ( Date: Thu, 18 Jun 2026 18:27:31 -0500 Subject: [PATCH 11/29] docs: remove styled-components references from cursorrules Describe Tailwind as the sole styling system and document the hybrid inline-utility + c-* recipe convention. Note that semantic attributes (role, aria-*) must be preserved when restyling. Co-Authored-By: Claude Opus 4.8 --- .cursorrules/README.md | 4 ++-- .cursorrules/styling.md | 27 ++++++++++++++++++++++++--- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/.cursorrules/README.md b/.cursorrules/README.md index c3a18908a..e86aed24d 100644 --- a/.cursorrules/README.md +++ b/.cursorrules/README.md @@ -11,7 +11,7 @@ You are an expert full-stack developer working on Compass, a calendar applicatio This is a monorepo using Bun workspaces with the following packages: -- `@compass/web` - React/TypeScript frontend with Redux, styled-components, webpack +- `@compass/web` - React/TypeScript frontend with Redux, Tailwind CSS - `@compass/backend` - Express.js REST API with MongoDB, Google Calendar sync, WebSocket support - `@compass/core` - Shared utilities, types, and business logic - `@compass/scripts` - CLI tools for building, database operations, user management @@ -42,7 +42,7 @@ This directory contains focused rules for different aspects of development: ``` packages/ ├── backend/src/ # Express.js API, MongoDB, Google Calendar sync -├── web/src/ # React frontend, Redux state, styled-components +├── web/src/ # React frontend, Redux state, Tailwind CSS ├── core/src/ # Shared utilities, types, business logic └── scripts/src/ # CLI tools for builds and operations ``` diff --git a/.cursorrules/styling.md b/.cursorrules/styling.md index 0b8a416c9..58329a0de 100644 --- a/.cursorrules/styling.md +++ b/.cursorrules/styling.md @@ -60,9 +60,30 @@ The pattern is: CSS variable `--color-{category}-{name}` → Tailwind class `{ca ### Styling Approach -- Use Tailwind utility classes directly in JSX -- For complex styles, use styled-components (already configured) -- Follow existing patterns in `packages/web/src/components/` +Tailwind is the **sole** styling system. Do not introduce any CSS-in-JS or +runtime styling library; a guard test (`no-styled-components.test.ts`) enforces +this. + +Use a hybrid of inline utilities and `c-*` recipes: + +- **Inline Tailwind utilities** for one-off layout and state, applied directly + in JSX (e.g. `className="mb-2.5 items-center justify-end gap-[30px]"`). +- **`c-*` utilities** for reusable component recipes and complex third-party + selectors (datepicker internals, animations, calendar grid selectors). These + are defined with Tailwind v4's `@utility c-...` directive in + `packages/web/src/index.css` (e.g. `c-event-form`, `c-button`, + `c-calendar-now-line`). Name reusable recipes with the `c-` prefix. +- **Semantic CSS-variable tokens** for all theme-dependent colors (see above), + so a future `[data-theme="light"]` rollout needs no component changes. +- **Inline CSS custom properties** only for runtime values that cannot be known + at build time — event colors, positions, and dynamic grid counts (e.g. + `style={{ "--event-form-bg": color }}`). + +Follow existing patterns in `packages/web/src/components/` and the recipes in +`packages/web/src/index.css`. + +Preserve semantic attributes (`role`, `aria-*`, `title`) when restyling — they +are load-bearing for assistive tech and e2e selectors, not presentation. ## Module Imports From c3ddaac26160006ab58b9c2e8d09da2139aa589e Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Thu, 18 Jun 2026 19:14:53 -0500 Subject: [PATCH 12/29] docs: remove styled-components migration design spec Drop the internal design spec so it is not carried into the repo. Co-Authored-By: Claude Opus 4.8 --- ...18-styled-components-to-tailwind-design.md | 133 ------------------ 1 file changed, 133 deletions(-) delete mode 100644 docs/superpowers/specs/2026-06-18-styled-components-to-tailwind-design.md diff --git a/docs/superpowers/specs/2026-06-18-styled-components-to-tailwind-design.md b/docs/superpowers/specs/2026-06-18-styled-components-to-tailwind-design.md deleted file mode 100644 index 321579a49..000000000 --- a/docs/superpowers/specs/2026-06-18-styled-components-to-tailwind-design.md +++ /dev/null @@ -1,133 +0,0 @@ -# Styled Components to Tailwind Migration Design - -## Objective - -Replace every Compass web use of `styled-components` with Tailwind CSS v4 while preserving the current visual design, interaction behavior, accessibility, and responsive layout. The finished repository must not depend on, configure, import, document, or test through `styled-components`. - -The migration must also leave theme-dependent styling behind semantic CSS custom properties so a future light theme can change token values without rewriting component classes. - -## Current State - -The web package currently combines two styling systems: - -- Tailwind v4 utilities and semantic tokens in `packages/web/src/index.css` -- `styled-components` across 60 source and test files, including shared primitives, forms, date pickers, menus, loaders, icons, Planner Sidebar surfaces, calendar grids, Week view, provider setup, and tests - -`packages/web/src/common/styles/theme.ts` is also used directly by non-styled TypeScript code for calculated colors, third-party component style objects, interaction overlays, and geometry constants. Removing the React theme provider does not require removing this plain TypeScript token adapter in the same step. - -## Chosen Approach - -Use a hybrid Tailwind architecture: - -1. Put one-off layout and state styling directly in JSX as Tailwind utilities. -2. Define stable reusable component recipes as `c-*` utilities in `packages/web/src/index.css`. -3. Use nested selectors inside `c-*` utilities for third-party DOM that Compass does not render directly, such as `react-datepicker` internals. -4. Pass runtime-only values through typed inline CSS custom properties, then consume them from static Tailwind classes or `c-*` utilities. -5. Keep all theme-dependent values behind semantic CSS custom properties. - -This avoids replacing `styled-components` with a large global component stylesheet while also avoiding unreadable repeated class strings for complex widgets. - -## Class Naming Rules - -Use `c-*` only for named, reusable Compass component recipes or complex selector scopes. Examples include `c-date-picker`, `c-context-menu-item`, and the existing `c-focus-ring`. - -Use regular Tailwind utilities for local layout, spacing, typography, and state. Do not prefix every Tailwind utility with `c-`; the prefix identifies Compass-owned abstractions rather than Tailwind itself. - -When a recipe has variants, prefer data attributes or CSS custom properties over generating class names dynamically. All class names must remain statically discoverable by Tailwind unless explicitly safelisted. - -## Theme Architecture - -`packages/web/src/index.css` remains the CSS source of truth for visual tokens. Components use semantic utilities such as `bg-bg-primary`, `text-text-light`, and `border-grid-line-primary`, never raw palette colors when a semantic token exists. - -The default dark token values remain unchanged. Token declarations will be structured so a future selector such as `[data-theme="light"]` can override the same custom properties. Adding a light theme or theme switcher is outside this migration. - -Runtime event colors, measured positions, visible-date counts, and similar values cannot be represented by a finite semantic palette. Components will expose those values as custom properties such as `--event-color` or `--visible-date-count`; static classes will reference the custom properties. - -`theme.ts` may remain as a plain typed object while non-CSS algorithms and third-party style APIs require JavaScript values. It must no longer import or implement `DefaultTheme`, and React must no longer require `ThemeProvider`. A later project may consolidate remaining JavaScript consumers onto exported token constants, but that is not required to remove `styled-components`. - -## Component Migration Strategy - -Migrate in dependency order: - -1. Shared primitives and icons: `Flex`, `Text`, `Input`, `Textarea`, `Button`, `IconButton`, `Divider`, `Focusable`, spinners, and icon wrappers. -2. Shared composite UI: context menus, loaders, date picker, and Not Found. -3. Forms: event form, recurrence controls, time/date controls, actions menu, and Someday form. -4. Planner Sidebar surfaces, including month picker and Someday event rows. -5. Shared calendar grid and Week view surfaces, including all-day rows, timed grids, headers, reminders, and edge indicators. -6. Provider and test cleanup after no rendered component depends on the styled theme context. - -Keep existing public component props when they express behavior. Styling-only props should become local class decisions, data attributes, or CSS custom properties. Avoid new barrel files. React components remain in their own files where a wrapper has behavior or a reusable API; trivial styled wrappers should collapse into their call site. - -## Fidelity Requirements - -The migration is not a redesign. Preserve: - -- element semantics and accessible names -- keyboard focus behavior and focus appearance -- hover, active, disabled, pending, selected, and drag states -- widths, heights, spacing, typography, borders, shadows, gradients, and stacking -- animation timing and reduced-motion behavior -- responsive and container-query behavior -- calendar event positioning, clipping, scrolling, and interaction hit areas -- third-party widget appearance and portal behavior - -Equivalent generated CSS is acceptable; DOM changes are acceptable only when they preserve semantics, event propagation, focus management, measurement, and layout. - -## Testing Strategy - -Use existing behavior tests as characterization coverage and add focused regression tests before changing styling behavior that is not currently protected. Tests should assert user-observable behavior, semantic state, or computed style through the existing Tailwind test stylesheet rather than coupling broadly to full class strings. - -For each migration slice: - -1. Run the smallest relevant web tests before changing the slice. -2. Add a failing characterization or regression test where a dynamic visual state lacks coverage. -3. Migrate the slice. -4. Run its focused tests and type checking. - -At the end, add or retain a repository guard that detects `styled-components` imports, manifest dependencies, Babel configuration, and obsolete documentation references. Then run: - -- `bun run test` -- `bun run test:e2e` -- `bun type-check` -- `bun lint` -- React Doctor on the changed React files -- `bun build:web` -- `bun build:backend` - -The full test commands may require local port access for MongoDB Memory Server and Playwright. Existing unrelated local worktrees must not be deleted; Jest discovery must instead be run in a way that excludes them or with those worktrees moved outside the repository by their owner. - -## Dependency and Configuration Removal - -After all runtime and test imports are gone: - -- remove `ThemeProvider` from `CompassProvider` and test wrappers -- remove `packages/web/src/common/styles/default-theme.d.ts` -- remove the styled-components Babel plugin configuration -- uninstall `styled-components` and `@types/styled-components` -- remove `babel-plugin-styled-components` if it is no longer a transitive or direct requirement -- regenerate `bun.lock` through Bun rather than editing lockfile entries manually -- verify a repository-wide search has no project-owned styled-components references - -## Documentation Changes - -Update project-owned documentation and agent guidance, including `CONTEXT.md`, `docs/Frontend/frontend-runtime-flow.md`, and `.cursorrules`, to describe Tailwind v4 as the web styling system. Document: - -- semantic theme tokens in `packages/web/src/index.css` -- when to create a `c-*` utility -- when to use local Tailwind utilities -- how runtime CSS custom properties are used -- the prohibition on raw colors when semantic tokens exist - -Vendored skill reference material that discusses CSS-in-JS generically is not Compass product documentation and is outside this migration. - -## Completion Criteria - -The migration is complete only when all of the following are proven from the current worktree: - -- no Compass runtime or test code imports `styled-components` -- no Compass manifest or build configuration depends on it -- project-owned documentation no longer describes or recommends it -- migrated UI behavior and visual states remain covered and pass -- full unit/package tests and Playwright e2e tests pass locally -- type checking, lint, React Doctor, web build, and backend build pass -- semantic CSS variables cover theme-dependent values, allowing future theme overrides without component rewrites From 1772adc032bfd3b209da1413ccba19edaf01a6e8 Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Thu, 18 Jun 2026 19:39:18 -0500 Subject: [PATCH 13/29] chore: cleanup --- .cursorrules/styling.md | 4 --- .../styles/no-styled-components.test.ts | 36 ------------------- packages/web/src/index.css | 16 ++++----- 3 files changed, 8 insertions(+), 48 deletions(-) delete mode 100644 packages/web/src/common/styles/no-styled-components.test.ts diff --git a/.cursorrules/styling.md b/.cursorrules/styling.md index 58329a0de..578360b1a 100644 --- a/.cursorrules/styling.md +++ b/.cursorrules/styling.md @@ -60,10 +60,6 @@ The pattern is: CSS variable `--color-{category}-{name}` → Tailwind class `{ca ### Styling Approach -Tailwind is the **sole** styling system. Do not introduce any CSS-in-JS or -runtime styling library; a guard test (`no-styled-components.test.ts`) enforces -this. - Use a hybrid of inline utilities and `c-*` recipes: - **Inline Tailwind utilities** for one-off layout and state, applied directly diff --git a/packages/web/src/common/styles/no-styled-components.test.ts b/packages/web/src/common/styles/no-styled-components.test.ts deleted file mode 100644 index d75ad04e3..000000000 --- a/packages/web/src/common/styles/no-styled-components.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { expect, test } from "bun:test"; -import { readdirSync, readFileSync, statSync } from "node:fs"; -import { join, resolve } from "node:path"; - -const srcRoot = join(import.meta.dir, "../.."); -const files = (directory: string): string[] => - readdirSync(directory).flatMap((name) => { - const path = join(directory, name); - return statSync(path).isDirectory() ? files(path) : [path]; - }); - -test("web source has no styled-components imports", () => { - const packageName = ["styled", "components"].join("-"); - const offenders = files(srcRoot).filter( - (file) => - /\.[jt]sx?$/.test(file) && - readFileSync(file, "utf8").includes(`from "${packageName}"`), - ); - expect(offenders).toEqual([]); -}); - -test("the removed styling package and Babel plugin stay absent", () => { - const repositoryRoot = resolve(srcRoot, "../../.."); - const manifest = readFileSync( - join(repositoryRoot, "packages/web/package.json"), - "utf8", - ); - const babelConfig = readFileSync( - join(repositoryRoot, "babel.config.js"), - "utf8", - ); - const packageName = ["styled", "components"].join("-"); - - expect(manifest).not.toContain(packageName); - expect(babelConfig).not.toContain(`babel-plugin-${packageName}`); -}); diff --git a/packages/web/src/index.css b/packages/web/src/index.css index 9de9e0139..d2f9b7e07 100644 --- a/packages/web/src/index.css +++ b/packages/web/src/index.css @@ -393,7 +393,7 @@ } @utility c-login-progress { - @apply relative h-[3px] w-full overflow-hidden rounded-[3px] bg-border-primary; + @apply relative h-0.75 w-full overflow-hidden rounded-[3px] bg-border-primary; &::after { @apply absolute top-0 h-full w-[45%] animate-login-progress content-[""]; @@ -442,7 +442,7 @@ } @utility c-date-picker { - @apply select-none rounded-[2px] border-0 font-medium shadow-[0_4px_4px_var(--color-shadow-default)]; + @apply select-none rounded-xs border-0 font-medium shadow-[0_4px_4px_var(--color-shadow-default)]; background-color: var(--date-picker-bg); font-size: 12px; @@ -704,7 +704,7 @@ } @utility c-event-form-description { - @apply relative max-h-[180px] border-hidden bg-transparent font-normal transition-all duration-300; + @apply relative max-h-45 border-hidden bg-transparent font-normal transition-all duration-300; font-size: var(--font-size-xxl); width: calc(100% - 20px); &:hover { @@ -714,7 +714,7 @@ } @utility c-time-picker { - @apply relative min-w-[90px]; + @apply relative min-w-22.5; font-size: var(--font-size-l); & span[aria-live="polite"], & .timepicker__indicators { @@ -818,7 +818,7 @@ } @utility c-recurrence-interval { - @apply ml-1 h-[38px] w-8 rounded-sm border border-transparent px-1 text-center transition-all duration-300; + @apply ml-1 h-9.5 w-8 rounded-sm border border-transparent px-1 text-center transition-all duration-300; background: var(--recurrence-bg); font-size: var(--font-size-s); &::-webkit-outer-spin-button, @@ -901,7 +901,7 @@ } @utility c-someday-event { - @apply w-full cursor-grab rounded-[2px] px-2 py-[3px] text-xs text-text-lighter transition-[background-color_.2s,opacity_.12s,box-shadow_.2s] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent-primary; + @apply w-full cursor-grab rounded-xs px-2 py-0.75 text-xs text-text-lighter transition-[background-color_.2s,opacity_.12s,box-shadow_.2s] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent-primary; background: var(--someday-event-bg); &[data-dragging="true"] { pointer-events: none; @@ -929,7 +929,7 @@ } @utility c-week-grid-track { - @apply relative flex h-full w-full min-w-[704px] flex-col; + @apply relative flex h-full w-full min-w-176 flex-col; container: week-grid-track / inline-size; } @@ -1024,7 +1024,7 @@ } @utility c-reminder-wrapper { - @apply relative inline-block pb-[18px]; + @apply relative inline-block pb-4.5; } @utility c-reminder-char-counter { From d578f9d1cc608fb8f7a58fb26df8e26edd9faf7d Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Thu, 18 Jun 2026 19:56:41 -0500 Subject: [PATCH 14/29] test: delete temp styled-components tests --- .../common/styles/composites-tailwind.test.ts | 41 ------------ .../src/common/styles/forms-tailwind.test.ts | 50 --------------- .../common/styles/planner-tailwind.test.ts | 27 -------- .../common/styles/primitives-tailwind.test.ts | 62 ------------------- .../src/common/styles/week-tailwind.test.ts | 35 ----------- 5 files changed, 215 deletions(-) delete mode 100644 packages/web/src/common/styles/composites-tailwind.test.ts delete mode 100644 packages/web/src/common/styles/forms-tailwind.test.ts delete mode 100644 packages/web/src/common/styles/planner-tailwind.test.ts delete mode 100644 packages/web/src/common/styles/primitives-tailwind.test.ts delete mode 100644 packages/web/src/common/styles/week-tailwind.test.ts diff --git a/packages/web/src/common/styles/composites-tailwind.test.ts b/packages/web/src/common/styles/composites-tailwind.test.ts deleted file mode 100644 index f8ab3f6d3..000000000 --- a/packages/web/src/common/styles/composites-tailwind.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { describe, expect, it } from "bun:test"; -import { existsSync, readFileSync } from "node:fs"; -import { join } from "node:path"; - -const webSrc = join(import.meta.dir, "../.."); - -const migratedFiles = [ - "components/AbsoluteOverflowLoader/AbsoluteOverflowLoader.tsx", - "components/LoginAbsoluteOverflowLoader/LoginAbsoluteOverflowLoader.tsx", - "components/ContextMenu/ContextMenu.tsx", - "components/ContextMenu/ContextMenuItems.tsx", - "components/DatePicker/DatePicker.tsx", - "views/NotFound/NotFound.tsx", -] as const; - -const removedFiles = [ - "components/AbsoluteOverflowLoader/styled.ts", - "components/LoginAbsoluteOverflowLoader/styled.ts", - "components/ContextMenu/styled.ts", - "components/DatePicker/styled.ts", - "views/NotFound/styled.ts", -] as const; - -describe("Tailwind composite component architecture", () => { - it("keeps shared composite components independent from styled-components", () => { - for (const file of migratedFiles) { - const path = join(webSrc, file); - - expect(existsSync(path), `${file} should exist`).toBe(true); - expect(readFileSync(path, "utf8"), file).not.toContain( - "styled-components", - ); - } - - for (const file of removedFiles) { - expect(existsSync(join(webSrc, file)), `${file} should be removed`).toBe( - false, - ); - } - }); -}); diff --git a/packages/web/src/common/styles/forms-tailwind.test.ts b/packages/web/src/common/styles/forms-tailwind.test.ts deleted file mode 100644 index d0b67d6f3..000000000 --- a/packages/web/src/common/styles/forms-tailwind.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { existsSync, readFileSync } from "node:fs"; -import { join } from "node:path"; - -const formsRoot = join(import.meta.dir, "../../views/Forms"); - -const productionFiles = [ - "ActionsMenu/ActionsMenu.tsx", - "ActionsMenu/MenuItem.tsx", - "EventForm/EventForm.tsx", - "EventForm/MoveToSidebarMenuButton.tsx", - "EventForm/SaveSection/SaveSection.tsx", - "EventForm/PrioritySection/PrioritySection.tsx", - "EventForm/DateControlsSection/DateControlsSection/DateControlsSection.tsx", - "EventForm/DateControlsSection/DateTimeSection/DateTimeSection.tsx", - "EventForm/DateControlsSection/DateTimeSection/DatePickers/DatePickers.tsx", - "EventForm/DateControlsSection/DateTimeSection/TimePicker/TimePicker.tsx", - "EventForm/DateControlsSection/DateTimeSection/TimePicker/TimePickers.tsx", - "EventForm/DateControlsSection/RecurrenceSection/RecurrenceSection.tsx", - "SomedayEventForm/SomedayEventForm.tsx", - "SomedayEventForm/SomedayRecurrenceSection/SomedayRecurrenceSection.tsx", - "SomedayEventForm/SomedayRecurrenceSection/SomedayRecurrenceSelect/SomedayRecurrenceSelect.tsx", -]; - -const removedStyleModules = [ - "ActionsMenu/styled.ts", - "EventForm/styled.ts", - "EventForm/PrioritySection/styled.ts", - "EventForm/DateControlsSection/DateControlsSection/styled.ts", - "EventForm/DateControlsSection/DateTimeSection/styled.ts", - "EventForm/DateControlsSection/DateTimeSection/DatePickers/styled.ts", - "EventForm/DateControlsSection/DateTimeSection/TimePicker/styled.ts", - "EventForm/DateControlsSection/RecurrenceSection/styled.ts", - "SomedayEventForm/styled.ts", - "SomedayEventForm/SomedayRecurrenceSection/SomedayRecurrenceSelect/styled.ts", -]; - -describe("forms Tailwind migration", () => { - test.each( - productionFiles, - )("%s has no styled-components dependency", (file) => { - expect(readFileSync(join(formsRoot, file), "utf8")).not.toMatch( - /styled-components|\/styled["']/, - ); - }); - - test.each(removedStyleModules)("removes %s", (file) => { - expect(existsSync(join(formsRoot, file))).toBe(false); - }); -}); diff --git a/packages/web/src/common/styles/planner-tailwind.test.ts b/packages/web/src/common/styles/planner-tailwind.test.ts deleted file mode 100644 index 7ebcdaf0a..000000000 --- a/packages/web/src/common/styles/planner-tailwind.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { existsSync, readFileSync } from "node:fs"; -import { join } from "node:path"; - -const webRoot = join(import.meta.dir, "../.."); - -describe("Planner Tailwind migration", () => { - test.each([ - "components/PlannerSidebar/PlannerMonthPicker/PlannerMonthPicker.tsx", - "components/PlannerSidebar/SomedayEventSections/SomedayEvents/SomedayEvent/SomedayEvent.tsx", - ])("%s has no styled-components dependency", (file) => { - expect(readFileSync(join(webRoot, file), "utf8")).not.toMatch( - /styled-components|\/styled["']/, - ); - }); - - test("removes the someday event styled module", () => { - expect( - existsSync( - join( - webRoot, - "components/PlannerSidebar/SomedayEventSections/SomedayEvents/SomedayEvent/styled.ts", - ), - ), - ).toBe(false); - }); -}); diff --git a/packages/web/src/common/styles/primitives-tailwind.test.ts b/packages/web/src/common/styles/primitives-tailwind.test.ts deleted file mode 100644 index c4e6f181c..000000000 --- a/packages/web/src/common/styles/primitives-tailwind.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { describe, expect, it } from "bun:test"; -import { existsSync, readFileSync } from "node:fs"; -import { join } from "node:path"; - -const webSrc = join(import.meta.dir, "../.."); - -const migratedFiles = [ - "components/Flex/Flex.tsx", - "components/Text/Text.tsx", - "components/Input/Input.tsx", - "components/Textarea/Textarea.tsx", - "components/IconButton/IconButton.tsx", - "components/Divider/Divider.tsx", - "components/Focusable/Focusable.tsx", - "components/Button/Button.tsx", - "components/Icons/Calendar.tsx", - "components/Icons/CircleTwo.tsx", - "components/Icons/Command.tsx", - "components/Icons/Flask.tsx", - "components/Icons/List.tsx", - "components/Icons/Refresh.tsx", - "components/Icons/Repeat.tsx", - "components/Icons/Sidebar.tsx", - "components/Icons/Spinner.tsx", - "components/Icons/Todo.tsx", - "components/Icons/X.tsx", -] as const; - -const removedFiles = [ - "components/Flex/index.ts", - "components/Flex/styled.ts", - "components/Text/index.ts", - "components/Text/styled.ts", - "components/Input/styled.ts", - "components/Textarea/styled.ts", - "components/Textarea/index.ts", - "components/IconButton/styled.ts", - "components/Divider/styled.ts", - "components/Divider/index.ts", - "components/Button/styled.ts", - "components/Icons/styled.ts", - "common/styles/animations/rotate.ts", -] as const; - -describe("Tailwind primitive architecture", () => { - it("keeps shared primitives independent from styled-components", () => { - for (const file of migratedFiles) { - const path = join(webSrc, file); - - expect(existsSync(path), `${file} should exist`).toBe(true); - expect(readFileSync(path, "utf8"), file).not.toContain( - "styled-components", - ); - } - - for (const file of removedFiles) { - expect(existsSync(join(webSrc, file)), `${file} should be removed`).toBe( - false, - ); - } - }); -}); diff --git a/packages/web/src/common/styles/week-tailwind.test.ts b/packages/web/src/common/styles/week-tailwind.test.ts deleted file mode 100644 index 57350d4be..000000000 --- a/packages/web/src/common/styles/week-tailwind.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { existsSync, readFileSync } from "node:fs"; -import { join } from "node:path"; - -const webRoot = join(import.meta.dir, "../.."); -const productionFiles = [ - "views/Week/WeekView.tsx", - "views/Week/components/Grid/WeekGridScrollArea.tsx", - "views/Week/components/Grid/AllDayRow/AllDayEvents.tsx", - "views/Week/components/Grid/AllDayRow/AllDayRow.tsx", - "views/Week/components/Header/DayLabels.tsx", - "views/Week/components/Header/Reminder/Reminder.tsx", - "views/Week/components/Grid/MainGrid/EdgeNavigationIndicators/EdgeNavigationIndicators.tsx", -]; -const styledModules = [ - "views/Week/styled.tsx", - "views/Week/components/Grid/Columns/styled.ts", - "views/Week/components/Grid/AllDayRow/styled.ts", - "views/Week/components/Header/Reminder/styled.ts", - "views/Week/components/Grid/MainGrid/EdgeNavigationIndicators/styled.ts", -]; - -describe("Week Tailwind migration", () => { - test.each( - productionFiles, - )("%s has no styled-components dependency", (file) => { - expect(readFileSync(join(webRoot, file), "utf8")).not.toMatch( - /styled-components|\/styled["']/, - ); - }); - - test.each(styledModules)("removes %s", (file) => { - expect(existsSync(join(webRoot, file))).toBe(false); - }); -}); From 7b53f878b1e4f2aa5a077189ce9f625982eda103 Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Fri, 19 Jun 2026 16:29:53 -0500 Subject: [PATCH 15/29] refactor(web): remove dead reminder CSS utilities The reminder feature was deleted (#1875); its c-reminder-* utilities and keyframes in index.css had no remaining consumers. Remove 7 utilities and 4 keyframes (~138 lines). Co-Authored-By: Claude Opus 4.8 --- packages/web/src/index.css | 138 ------------------------------------- 1 file changed, 138 deletions(-) diff --git a/packages/web/src/index.css b/packages/web/src/index.css index d2f9b7e07..2936c20f2 100644 --- a/packages/web/src/index.css +++ b/packages/web/src/index.css @@ -977,144 +977,6 @@ } } -@keyframes c-reminder-underline-draw { - from { - stroke-dashoffset: 1000; - opacity: 0.7; - } - to { - stroke-dashoffset: 0; - opacity: 1; - } -} - -@keyframes c-reminder-fade-in { - from { - opacity: 0; - transform: translateY(10px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -@keyframes c-reminder-gradient-wave { - 0%, - 100% { - background-position: 0% 50%; - } - 50% { - background-position: 100% 50%; - } -} - -@keyframes c-reminder-shadow { - 0%, - 100% { - filter: drop-shadow(0 0 2px var(--compass-color-bg-primary)); - } - 50% { - filter: drop-shadow(0 0 4px var(--compass-color-bg-secondary)); - } -} - -@utility c-reminder-container { - @apply z-1 mx-auto flex h-full w-full items-center justify-center; -} - -@utility c-reminder-wrapper { - @apply relative inline-block pb-4.5; -} - -@utility c-reminder-char-counter { - @apply absolute right-0 bottom-[-2px] left-0 w-full text-center text-xs opacity-80; - color: var(--compass-color-text-light-inactive); - &[data-near-limit="true"] { - color: var(--compass-color-status-warning); - } -} - -@utility c-reminder-placeholder { - @apply relative cursor-pointer px-2.5 text-center text-[28px] italic; - color: var(--compass-color-text-light-inactive); - font-family: "Caveat", cursive; - animation: c-reminder-fade-in 0.5s ease-out; - &:hover { - color: var(--compass-color-text-light); - transition: color 0.3s ease; - } -} - -@utility c-reminder-text { - @apply relative min-w-[200px] max-w-[500px] cursor-pointer px-2.5 text-center font-semibold whitespace-pre-wrap break-words; - color: transparent; - background: linear-gradient( - 90deg, - var(--compass-color-gradient-accent-light-start), - var(--compass-color-gradient-accent-light-end) - ); - background-size: 200% 200%; - background-clip: text; - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - font-family: "Caveat", cursive; - font-size: 28px; - line-height: 1.3; - filter: drop-shadow(0 0 2px var(--compass-color-bg-primary)); - animation: - c-reminder-fade-in 0.5s ease-out, - c-reminder-gradient-wave 15s ease infinite; - &[data-length="medium"] { - font-size: 24px; - } - &[data-length="long"] { - font-size: 22px; - } - &[data-editing="true"] { - max-height: 80px; - cursor: text; - overflow: hidden; - outline: none; - } - &[data-editing="false"] { - max-height: 80px; - overflow-y: auto; - } - @media (prefers-reduced-motion: no-preference) { - animation: - c-reminder-shadow 5s ease-in-out infinite, - c-reminder-fade-in 0.5s ease-out, - c-reminder-gradient-wave 15s ease infinite; - } -} - -@utility c-reminder-underline { - @apply pointer-events-none absolute bottom-0 left-[-5px] h-5 opacity-0; - width: calc(100% + 10px); - &[data-visible="true"] { - opacity: 1; - } - & path { - fill: none; - stroke-width: 3; - stroke-linecap: round; - stroke-linejoin: round; - stroke-dasharray: 1000; - stroke-dashoffset: 1000; - } - &[data-visible="true"] path { - animation: c-reminder-underline-draw 0.8s ease-out forwards; - } -} - -@utility c-reminder-placeholder-underline { - @apply c-reminder-underline; - & path { - stroke-width: 2.5; - } -} - @utility c-calendar-all-day-columns { @apply absolute top-0 grid h-full; left: var(--calendar-grid-margin-left); From 9bd767efbd98f63ca311f26521e726c3f2a9c27b Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Fri, 19 Jun 2026 16:33:34 -0500 Subject: [PATCH 16/29] refactor(web): inline one-off layout utilities Replace single-use c-* layout recipes with inline Tailwind utilities, removing the global-CSS indirection for styling used in exactly one place: c-floating-form-container, c-calendar-main-grid, c-calendar-grid-rows, c-calendar-timed-columns, c-week-columns, c-week-grid-track. No visual change; theme tokens and semantic attributes unchanged. Co-Authored-By: Claude Opus 4.8 --- .../components/CalendarTimedGrid.tsx | 6 ++-- packages/web/src/index.css | 36 ------------------- .../FloatingFormContainer.tsx | 2 +- packages/web/src/views/Week/WeekView.tsx | 2 +- .../Week/components/Header/DayLabels.tsx | 2 +- 5 files changed, 6 insertions(+), 42 deletions(-) diff --git a/packages/web/src/common/calendar-grid/components/CalendarTimedGrid.tsx b/packages/web/src/common/calendar-grid/components/CalendarTimedGrid.tsx index 05acc0c17..b0e1e14fd 100644 --- a/packages/web/src/common/calendar-grid/components/CalendarTimedGrid.tsx +++ b/packages/web/src/common/calendar-grid/components/CalendarTimedGrid.tsx @@ -58,7 +58,7 @@ export const CalendarTimedGrid: FC = ({ return (
= ({ >
= ({ ))}
-
+
{getHourLabels(true).map((dayTime) => ( (({ strategy, left, top, style, ...props }, ref) => (
{
-
+
= ({ return (
-
+
{weekDays.map((day) => { const dayNumber = getDayNumber(day); const { isToday, color } = getColor(day); From ff6e26878742fb90ca18b8ca625fbcbf3688543e Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Fri, 19 Jun 2026 16:38:25 -0500 Subject: [PATCH 17/29] refactor(web): inline variant-based someday utilities Convert single-use c-someday-event and c-someday-recurrence-value recipes to inline Tailwind utilities using data-[...]/hover: variants. Keep c-week-edge-zone (its dual data-position gradient variants are a coherent, complex treatment better expressed as a named recipe). No visual change. Co-Authored-By: Claude Opus 4.8 --- .../SomedayEvent/SomedayEvent.tsx | 2 +- packages/web/src/index.css | 23 ------------------- .../SomedayRecurrenceSelect.tsx | 2 +- 3 files changed, 2 insertions(+), 25 deletions(-) diff --git a/packages/web/src/components/PlannerSidebar/SomedayEventSections/SomedayEvents/SomedayEvent/SomedayEvent.tsx b/packages/web/src/components/PlannerSidebar/SomedayEventSections/SomedayEvents/SomedayEvent/SomedayEvent.tsx index 63e1378ae..f8d7d915b 100644 --- a/packages/web/src/components/PlannerSidebar/SomedayEventSections/SomedayEvents/SomedayEvent/SomedayEvent.tsx +++ b/packages/web/src/components/PlannerSidebar/SomedayEventSections/SomedayEvents/SomedayEvent/SomedayEvent.tsx @@ -71,7 +71,7 @@ export const SomedayEvent = ({ return (
From 3620cdd194a7979d5a163ea55d5664b71438be76 Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Fri, 19 Jun 2026 16:44:48 -0500 Subject: [PATCH 18/29] refactor(web): inline calendar grid layout utilities Push descendant-selector recipes onto their child elements and inline the remaining single-use calendar grid layout: - c-week-day-labels: clamp font sizes moved onto the Text children - c-calendar-grid-row: inlined (its '& > span' rule was dead) - c-calendar-day-times: child height/display moved onto mapped children - c-calendar-now-line: inlined (trivial) - c-calendar-all-day-columns: inlined; pseudo grid-line via before: variant Keep c-calendar-date-column (2 consumers) and c-week-edge-zone (gradient variant set). No visual change; verified with web unit + 50/50 e2e. Co-Authored-By: Claude Opus 4.8 --- .../components/CalendarAllDayRow.tsx | 2 +- .../components/CalendarTimedGrid.tsx | 20 ++++--- packages/web/src/index.css | 58 ------------------- .../Week/components/Header/DayLabels.tsx | 13 +++-- 4 files changed, 23 insertions(+), 70 deletions(-) diff --git a/packages/web/src/common/calendar-grid/components/CalendarAllDayRow.tsx b/packages/web/src/common/calendar-grid/components/CalendarAllDayRow.tsx index 527d68879..07a397af9 100644 --- a/packages/web/src/common/calendar-grid/components/CalendarAllDayRow.tsx +++ b/packages/web/src/common/calendar-grid/components/CalendarAllDayRow.tsx @@ -61,7 +61,7 @@ export const CalendarAllDayRow: FC = ({ }} >
= ({ return (
= ({ >
= ({
{getHourLabels(true).map((dayTime) => ( { }, [currentHour]); return ( -
+
{hourLabels.map((label, index) => ( -
- {label} +
+ + {label} +
))}
@@ -149,7 +155,7 @@ const CalendarNowLine = () => { return (
span { - position: absolute; - bottom: -5px; - left: -50px; - } -} -@utility c-calendar-day-times { - @apply absolute h-full; - top: calc(100% / 13 - 5px); - z-index: 1; - & > div { - height: calc(100% / 13); - } - & > div > span { - display: block; - } -} -@utility c-calendar-now-line { - @apply absolute h-px w-full; -} diff --git a/packages/web/src/views/Week/components/Header/DayLabels.tsx b/packages/web/src/views/Week/components/Header/DayLabels.tsx index 8d9635dbc..b97a5a805 100644 --- a/packages/web/src/views/Week/components/Header/DayLabels.tsx +++ b/packages/web/src/views/Week/components/Header/DayLabels.tsx @@ -40,8 +40,8 @@ export const DayLabels: FC = ({ }; return ( -
-
+
+
{weekDays.map((day) => { const dayNumber = getDayNumber(day); const { isToday, color } = getColor(day); @@ -53,10 +53,15 @@ export const DayLabels: FC = ({ style={{ color }} title={getWeekDayLabel(day)} > - + {dayNumber} - {day.format("ddd")} + + {day.format("ddd")} +
); })} From 09073f912598687d2af4e81ee3a6fffe4b2acc64 Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Fri, 19 Jun 2026 16:45:35 -0500 Subject: [PATCH 19/29] docs: encode the inline-first c-* decision test in styling rules Document that inline is the default and a c-* recipe is justified only when inline can't express it (third-party descendant selectors, pseudo-elements, keyframe bundles, or genuine reuse). Refresh stale examples. Co-Authored-By: Claude Opus 4.8 --- .cursorrules/styling.md | 39 +++++++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/.cursorrules/styling.md b/.cursorrules/styling.md index 578360b1a..823269886 100644 --- a/.cursorrules/styling.md +++ b/.cursorrules/styling.md @@ -60,19 +60,38 @@ The pattern is: CSS variable `--color-{category}-{name}` → Tailwind class `{ca ### Styling Approach -Use a hybrid of inline utilities and `c-*` recipes: - -- **Inline Tailwind utilities** for one-off layout and state, applied directly - in JSX (e.g. `className="mb-2.5 items-center justify-end gap-[30px]"`). -- **`c-*` utilities** for reusable component recipes and complex third-party - selectors (datepicker internals, animations, calendar grid selectors). These - are defined with Tailwind v4's `@utility c-...` directive in - `packages/web/src/index.css` (e.g. `c-event-form`, `c-button`, - `c-calendar-now-line`). Name reusable recipes with the `c-` prefix. +Use a hybrid of inline utilities and `c-*` recipes. **Inline is the default.** + +The deciding question for extracting a `c-*` recipe is *"can this be expressed +inline at all?"* — **not** "is it long?" or "is it used twice?". Reach for a +`c-*` recipe (Tailwind v4 `@utility c-...` in `packages/web/src/index.css`) only +when inline utilities genuinely cannot do the job, i.e. it has any of: + +1. **Third-party descendant selectors** you don't control — `.react-datepicker__*`, + `.timepicker__*` (e.g. `c-date-picker`, `c-time-picker`). +2. **Pseudo-elements / vendor scrollbars** — `::before`, `::after`, + `::-webkit-scrollbar` (e.g. `compass-scroll`). Prefer the Tailwind `before:` / + `after:` variants inline first; only extract if that gets unreadable. +3. **Keyframe-driven animation bundles** (e.g. `c-loader-spinner`). +4. **Genuine reuse** across components, or a coherent variant set + (e.g. `c-button*`, `c-event-form`). + +Otherwise, **inline it**: + +- **Inline Tailwind utilities** for one-off layout and state, in JSX directly + (e.g. `className="mb-2.5 items-center justify-end gap-[30px]"`). Use `data-[…]:` + and `hover:` variants instead of `&[data-…]`/`&:hover` recipe blocks. Arbitrary + values are fine for runtime vars: `grid-cols-[repeat(7,minmax(80px,1fr))]`, + `w-[calc(100%_-_50px)]`. +- If a recipe exists only to reach into children via `& .child` / `& > *`, put + the utilities **on the child elements** instead and delete the descendant rule. +- **Do not** create one-off `c-*` recipes named after their implementation + (`c-week-columns`, `c-calendar-grid-rows`); that just recreates a global CSS + layer. Inline them. - **Semantic CSS-variable tokens** for all theme-dependent colors (see above), so a future `[data-theme="light"]` rollout needs no component changes. - **Inline CSS custom properties** only for runtime values that cannot be known - at build time — event colors, positions, and dynamic grid counts (e.g. + at build time — event colors, positions, dynamic grid counts (e.g. `style={{ "--event-form-bg": color }}`). Follow existing patterns in `packages/web/src/components/` and the recipes in From 1e9587cb632cf3ba51cd9ae928e529165e003c40 Mon Sep 17 00:00:00 2001 From: Tyler Dane Date: Fri, 19 Jun 2026 17:06:58 -0500 Subject: [PATCH 20/29] refactor(web): inline rarely-reused c-* utilities Move single-use (or feature-local) recipes out of index.css to their call sites as inline Tailwind, trading minor duplication for a leaner global stylesheet: c-actions-menu(-item), c-recurrence-row/weekday/interval/caret, c-week-grid-scroller, c-week-edge-zone, c-calendar-date-column. Pseudo-element and data-state styling use arbitrary/data- variants. Kept c-time-picker and c-planner-month-picker as global recipes: they style third-party DOM (react-select/react-datepicker) that can't take Tailwind classes, and the test harness already special-cases them. Verified: web unit (1041) + 50/50 e2e + type-check + lint + build. Co-Authored-By: Claude Opus 4.8 --- .../components/CalendarAllDayRow.tsx | 2 +- .../components/CalendarTimedGrid.tsx | 2 +- packages/web/src/index.css | 171 +++--------------- .../views/Forms/ActionsMenu/ActionsMenu.tsx | 2 +- .../src/views/Forms/ActionsMenu/MenuItem.tsx | 2 +- .../RecurrenceSection/RecurrenceSection.tsx | 2 +- .../components/CaretInput.tsx | 4 +- .../components/EndsOnDate.tsx | 2 +- .../components/RecurrenceIntervalSelect.tsx | 4 +- .../components/RecurrenceToggle.tsx | 2 +- .../RecurrenceSection/components/WeekDay.tsx | 2 +- .../RecurrenceSection/components/WeekDays.tsx | 2 +- .../SomedayRecurrenceSection.tsx | 2 +- .../EdgeNavigationIndicators.tsx | 2 +- .../components/Grid/WeekGridScrollArea.tsx | 2 +- 15 files changed, 45 insertions(+), 158 deletions(-) diff --git a/packages/web/src/common/calendar-grid/components/CalendarAllDayRow.tsx b/packages/web/src/common/calendar-grid/components/CalendarAllDayRow.tsx index 07a397af9..ce540b00e 100644 --- a/packages/web/src/common/calendar-grid/components/CalendarAllDayRow.tsx +++ b/packages/web/src/common/calendar-grid/components/CalendarAllDayRow.tsx @@ -74,7 +74,7 @@ export const CalendarAllDayRow: FC = ({ > {visibleDates.map(({ date, key }) => (
= ({ {isTodayVisible ? : null} {visibleDates.map(({ date, key }) => (
= ({ returnFocus={false} >
= ({ role="menuitem" tabIndex={tabIndex} type={type} - className="c-actions-menu-item" + className="flex w-full cursor-pointer items-center gap-2 border-0 bg-[var(--actions-menu-item-bg)] px-2 py-1 text-left text-m text-text-dark outline-none hover:[text-shadow:0_0_0.5px_var(--compass-color-text-dark),0_0_0.5px_var(--compass-color-text-dark)] focus-visible:[text-shadow:0_0_0.5px_var(--compass-color-text-dark),0_0_0.5px_var(--compass-color-text-dark)]" style={{ backgroundColor: bgColor }} > {children} diff --git a/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/RecurrenceSection.tsx b/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/RecurrenceSection.tsx index 34c0894cf..909a5e4e6 100644 --- a/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/RecurrenceSection.tsx +++ b/packages/web/src/views/Forms/EventForm/DateControlsSection/RecurrenceSection/RecurrenceSection.tsx @@ -43,7 +43,7 @@ export function createRecurrenceSection({ const disabledMessage = "Sign in to use recurring events."; return ( -
+
{ return (
, })); mock.module("@web/views/Forms/EventForm/PrioritySection", () => ({ @@ -97,6 +97,32 @@ describe("EventForm", () => { document.body.removeAttribute("data-app-locked"); }); + it("renders the description before the actions on the same row", () => { + render( + , + ); + + const description = screen.getByPlaceholderText("Description"); + const actions = screen.getByRole("button", { name: "Event actions" }); + const row = description.parentElement?.parentElement; + + expect(row).toBe(actions.parentElement?.parentElement); + expect(row).toHaveClass("flex"); + expect(description.parentElement).toHaveClass("flex-1"); + expect(actions.parentElement).toHaveClass("shrink-0"); + expect(row?.firstElementChild?.contains(description)).toBe(true); + expect(row?.lastElementChild?.contains(actions)).toBe(true); + }); + it("duplicates the event with Mod+D while the title field is focused", async () => { const event = createEvent(); const onDuplicate = mock(); diff --git a/packages/web/src/views/Forms/EventForm/EventForm.tsx b/packages/web/src/views/Forms/EventForm/EventForm.tsx index 44f525f9f..c70b3a241 100644 --- a/packages/web/src/views/Forms/EventForm/EventForm.tsx +++ b/packages/web/src/views/Forms/EventForm/EventForm.tsx @@ -23,12 +23,12 @@ import { type SelectOption } from "@web/common/types/component.types"; import { mapToBackend } from "@web/common/utils/datetime/web.date.util"; import { getCategory } from "@web/common/utils/event/event.util"; import { isComboboxInteraction } from "@web/common/utils/form/form.util"; -import { Flex } from "@web/components/Flex/Flex"; import { Input } from "@web/components/Input/Input"; import { Textarea } from "@web/components/Textarea/Textarea"; import { DateControlsSection } from "@web/views/Forms/EventForm/DateControlsSection/DateControlsSection/DateControlsSection"; import { getFormDates } from "@web/views/Forms/EventForm/DateControlsSection/DateTimeSection/form.datetime.util"; import { RecurrenceSection } from "@web/views/Forms/EventForm/DateControlsSection/RecurrenceSection/RecurrenceSection"; +import { DescriptionActionsRow } from "@web/views/Forms/EventForm/DescriptionActionsRow"; import { EventActionMenu } from "@web/views/Forms/EventForm/EventActionMenu"; import { PrioritySection } from "@web/views/Forms/EventForm/PrioritySection"; import { SaveSection } from "@web/views/Forms/EventForm/SaveSection"; @@ -474,19 +474,6 @@ export const EventForm: React.FC> = memo( { "--event-form-bg": hoverColorByPriority[priority] } as CSSVariables } > - - { - onConvert?.(); - }} - onDuplicate={onDuplicateEvent} - onDelete={onDelete} - /> - - > = memo( -