From 7ba28db3a595f7e8cdd22712133013610dcad0d7 Mon Sep 17 00:00:00 2001
From: Evan Hu
Date: Fri, 18 Sep 2026 23:19:06 +0900
Subject: [PATCH 1/5] refactor(propel): move off the deprecated Base UI beta
onto @base-ui/react 1.7.0
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
packages/propel was built on `@base-ui-components/react@1.0.0-beta.3`, which pnpm-lock.yaml itself records as `deprecated: Package was renamed to @base-ui/react`. @makeplane/propel 0.3.0 already depends on `@base-ui/react@^1.6.0` and resolves to 1.7.0, so every bundle shipped two Base UI runtimes. After this change `pnpm why -r` reports one version of @base-ui/react, resolved to the same peer set as the one the external propel pulls, so the duplicate is gone from apps/web, apps/space and apps/admin alike. Doing the substrate move now rather than later matters because the migration onto @pace/propel is about to add hundreds of call sites on top of it.
The rename is not an identity mapping — 1.0 shipped between beta.3 and 1.7.0 — so each of the sixteen importing components was checked against the installed package rather than sed-ed. Four APIs actually moved. `Accordion.Root`'s `openMultiple` is now `multiple` (renamed in 1.0.0-beta.4). `Tooltip`'s `delay`/`closeDelay` and `Menu`'s `openOnHover` moved from the root onto the trigger in 1.0.0-beta.5, where detached triggers can each carry their own; the menu has three mutually exclusive trigger branches, so the prop is applied to each. `Tabs.Tab` renamed its `[data-selected]` attribute to `[data-active]` and its render-state key from `selected` to `active`, which the trigger's variant classes and the emoji picker's class callback both read. `Combobox.Root` now reports a cleared selection as `null`, which the public `onValueChange` signature does not carry, so the root maps it back to the empty value for the active mode.
`Popover` is the one place where an upstream change would have altered a @pace/propel prop contract: `openOnHover`, `delay` and `closeDelay` left `Popover.Root`, but three apps/web call sites pass them to @pace/propel's `Popover` (the calendar block, the gantt block and the editor user mention, all hover preview cards). Rather than edit apps in a dependency PR, the root keeps accepting them and forwards them to `Popover.Button` through a small context, so the component's public shape is unchanged.
Everything else survived the jump unchanged, verified attribute by attribute: the `--accordion-panel-height`, `--collapsible-panel-height`, `--active-tab-width`/`--active-tab-left` and `--toast-*` custom properties, `data-panel-open`, `data-side`/`data-align`, `data-starting-style`/`data-ending-style`, and the toast manager surface behind `setToast`/`updateToast`/`setPromiseToast`/`dismissToast`.
---
packages/propel/package.json | 2 +-
packages/propel/src/accordion/accordion.tsx | 4 +-
packages/propel/src/avatar/avatar.tsx | 2 +-
.../propel/src/collapsible/collapsible.tsx | 2 +-
packages/propel/src/combobox/combobox.tsx | 10 ++--
.../propel/src/context-menu/context-menu.tsx | 2 +-
packages/propel/src/dialog/root.tsx | 2 +-
.../src/emoji-icon-picker/emoji-picker.tsx | 8 +--
packages/propel/src/input/input.tsx | 2 +-
packages/propel/src/menu/menu.tsx | 8 ++-
packages/propel/src/popover/root.tsx | 28 +++++++--
packages/propel/src/scrollarea/scrollarea.tsx | 2 +-
packages/propel/src/separator/separator.tsx | 2 +-
packages/propel/src/switch/root.tsx | 2 +-
packages/propel/src/tabs/tabs.tsx | 4 +-
packages/propel/src/toast/toast.tsx | 2 +-
packages/propel/src/tooltip/root.tsx | 7 ++-
pnpm-lock.yaml | 58 ++-----------------
pnpm-workspace.yaml | 2 +-
19 files changed, 63 insertions(+), 86 deletions(-)
diff --git a/packages/propel/package.json b/packages/propel/package.json
index 1a404e58..f38b5392 100644
--- a/packages/propel/package.json
+++ b/packages/propel/package.json
@@ -63,7 +63,7 @@
"build-storybook": "storybook build"
},
"dependencies": {
- "@base-ui-components/react": "catalog:",
+ "@base-ui/react": "catalog:",
"@makeplane/propel": "catalog:",
"@pace/constants": "workspace:*",
"@pace/hooks": "workspace:*",
diff --git a/packages/propel/src/accordion/accordion.tsx b/packages/propel/src/accordion/accordion.tsx
index ac5071cf..979c18f1 100644
--- a/packages/propel/src/accordion/accordion.tsx
+++ b/packages/propel/src/accordion/accordion.tsx
@@ -5,7 +5,7 @@
*/
import * as React from "react";
-import { Accordion as BaseAccordion } from "@base-ui-components/react";
+import { Accordion as BaseAccordion } from "@base-ui/react";
import { AddOutline } from "@makeplane/propel/icons";
@@ -39,7 +39,7 @@ export interface AccordionContentProps {
function AccordionRoot({ defaultValue = [], allowMultiple = false, className = "", children }: AccordionRootProps) {
return (
-
+
{children}
);
diff --git a/packages/propel/src/avatar/avatar.tsx b/packages/propel/src/avatar/avatar.tsx
index 3f59a692..e4ce720f 100644
--- a/packages/propel/src/avatar/avatar.tsx
+++ b/packages/propel/src/avatar/avatar.tsx
@@ -5,7 +5,7 @@
*/
import React from "react";
-import { Avatar as AvatarPrimitive } from "@base-ui-components/react/avatar";
+import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar";
import { cn } from "../utils/classname";
export type TAvatarSize = "sm" | "md" | "base" | "lg" | number;
diff --git a/packages/propel/src/collapsible/collapsible.tsx b/packages/propel/src/collapsible/collapsible.tsx
index a92b1646..5802c45e 100644
--- a/packages/propel/src/collapsible/collapsible.tsx
+++ b/packages/propel/src/collapsible/collapsible.tsx
@@ -5,7 +5,7 @@
*/
import React, { useState, useEffect, useCallback, createContext, useContext } from "react";
-import { Collapsible as BaseCollapsible } from "@base-ui-components/react/collapsible";
+import { Collapsible as BaseCollapsible } from "@base-ui/react/collapsible";
import clsx from "clsx";
// Types
diff --git a/packages/propel/src/combobox/combobox.tsx b/packages/propel/src/combobox/combobox.tsx
index c4e83c05..dbe29cd4 100644
--- a/packages/propel/src/combobox/combobox.tsx
+++ b/packages/propel/src/combobox/combobox.tsx
@@ -5,7 +5,7 @@
*/
import * as React from "react";
-import { Combobox as BaseCombobox } from "@base-ui-components/react/combobox";
+import { Combobox as BaseCombobox } from "@base-ui/react/combobox";
import { SearchOutline } from "@makeplane/propel/icons";
import { cn } from "../utils/classname";
@@ -73,11 +73,13 @@ function ComboboxRoot({
onOpenChange,
children,
}: ComboboxProps) {
+ // Base UI 1.x reports a cleared selection as `null`, which the public `onValueChange` contract does not
+ // carry. Map it back to the empty value of whichever mode is active so callers keep seeing one shape.
const handleValueChange = React.useCallback(
- (newValue: string | string[]) => {
- onValueChange?.(newValue);
+ (newValue: string | string[] | null) => {
+ onValueChange?.(newValue ?? (multiSelect ? [] : ""));
},
- [onValueChange]
+ [onValueChange, multiSelect]
);
return (
diff --git a/packages/propel/src/context-menu/context-menu.tsx b/packages/propel/src/context-menu/context-menu.tsx
index e5d0211a..193bbf05 100644
--- a/packages/propel/src/context-menu/context-menu.tsx
+++ b/packages/propel/src/context-menu/context-menu.tsx
@@ -5,7 +5,7 @@
*/
import * as React from "react";
-import { ContextMenu as ContextMenuPrimitive } from "@base-ui-components/react/context-menu";
+import { ContextMenu as ContextMenuPrimitive } from "@base-ui/react/context-menu";
import { cn } from "../utils";
export interface ContextMenuProps extends React.ComponentProps {
diff --git a/packages/propel/src/dialog/root.tsx b/packages/propel/src/dialog/root.tsx
index 3a917b2c..ef6b94c9 100644
--- a/packages/propel/src/dialog/root.tsx
+++ b/packages/propel/src/dialog/root.tsx
@@ -5,7 +5,7 @@
*/
import { forwardRef, memo, useMemo } from "react";
-import { Dialog as BaseDialog } from "@base-ui-components/react";
+import { Dialog as BaseDialog } from "@base-ui/react";
import { cn } from "../utils/classname";
// enums
diff --git a/packages/propel/src/emoji-icon-picker/emoji-picker.tsx b/packages/propel/src/emoji-icon-picker/emoji-picker.tsx
index 9fc78127..40c4b09d 100644
--- a/packages/propel/src/emoji-icon-picker/emoji-picker.tsx
+++ b/packages/propel/src/emoji-icon-picker/emoji-picker.tsx
@@ -5,7 +5,7 @@
*/
import { useMemo, useCallback } from "react";
-import { Tabs } from "@base-ui-components/react";
+import { Tabs } from "@base-ui/react";
import { Popover } from "../popover";
import { cn } from "../utils/classname";
import { convertPlacementToSideAndAlign } from "../utils/placement";
@@ -131,10 +131,10 @@ export function EmojiPicker(props: TCustomEmojiPicker) {
+ className={({ active }) =>
cn("rounded-sm border border-subtle bg-layer-1 py-1 text-13", {
- "bg-surface-1 text-primary": selected,
- "text-placeholder hover:bg-layer-1/60 hover:text-tertiary": !selected,
+ "bg-surface-1 text-primary": active,
+ "text-placeholder hover:bg-layer-1/60 hover:text-tertiary": !active,
})
}
>
diff --git a/packages/propel/src/input/input.tsx b/packages/propel/src/input/input.tsx
index e6b8c094..1edb8adf 100644
--- a/packages/propel/src/input/input.tsx
+++ b/packages/propel/src/input/input.tsx
@@ -5,7 +5,7 @@
*/
import * as React from "react";
-import { Input as BaseInput } from "@base-ui-components/react/input";
+import { Input as BaseInput } from "@base-ui/react/input";
// helpers
import { cn } from "../utils";
diff --git a/packages/propel/src/menu/menu.tsx b/packages/propel/src/menu/menu.tsx
index a7446ded..135fa349 100644
--- a/packages/propel/src/menu/menu.tsx
+++ b/packages/propel/src/menu/menu.tsx
@@ -5,7 +5,7 @@
*/
import * as React from "react";
-import { Menu as BaseMenu } from "@base-ui-components/react/menu";
+import { Menu as BaseMenu } from "@base-ui/react/menu";
import { ChevronDownOutline, ChevronRightOutline, MoreHorizontalOutline } from "@makeplane/propel/icons";
import { cn } from "../utils/classname";
import type { TMenuProps, TSubMenuProps, TMenuItemProps } from "./types";
@@ -128,10 +128,12 @@ function Menu(props: TMenuProps) {
};
return (
-
+ // Base UI 1.x owns `openOnHover` on the trigger, not the root, so that one root can serve several detached triggers.
+
{customButton ? (
;
+
+const PopoverHoverContext = createContext({});
+
+export interface PopoverProps extends React.ComponentProps, TPopoverHoverProps {}
+
export interface PopoverContentProps extends React.ComponentProps {
placement?: TPlacement;
align?: TAlign;
@@ -52,7 +64,8 @@ const PopoverContent = memo(function PopoverContent({
// wrapper components
const PopoverTrigger = memo(function PopoverTrigger(props: React.ComponentProps) {
- return ;
+ const hoverProps = useContext(PopoverHoverContext);
+ return ;
});
const PopoverPortal = memo(function PopoverPortal(props: React.ComponentProps) {
@@ -65,8 +78,13 @@ const PopoverPositioner = memo(function PopoverPositioner(props: React.Component
// compound components
const Popover = Object.assign(
- memo(function Popover(props: React.ComponentProps) {
- return ;
+ memo(function Popover({ openOnHover, delay, closeDelay, ...props }: PopoverProps) {
+ const hoverProps = useMemo(() => ({ openOnHover, delay, closeDelay }), [openOnHover, delay, closeDelay]);
+ return (
+
+
+
+ );
}),
{
Button: PopoverTrigger,
diff --git a/packages/propel/src/scrollarea/scrollarea.tsx b/packages/propel/src/scrollarea/scrollarea.tsx
index 4c52e98d..14543223 100644
--- a/packages/propel/src/scrollarea/scrollarea.tsx
+++ b/packages/propel/src/scrollarea/scrollarea.tsx
@@ -5,7 +5,7 @@
*/
import * as React from "react";
-import { ScrollArea as BaseScrollArea } from "@base-ui-components/react/scroll-area";
+import { ScrollArea as BaseScrollArea } from "@base-ui/react/scroll-area";
import { cn } from "../utils/classname";
diff --git a/packages/propel/src/separator/separator.tsx b/packages/propel/src/separator/separator.tsx
index 22030e2a..a6ebcec2 100644
--- a/packages/propel/src/separator/separator.tsx
+++ b/packages/propel/src/separator/separator.tsx
@@ -5,7 +5,7 @@
*/
import * as React from "react";
-import { Separator as SeparatorPrimitive } from "@base-ui-components/react/separator";
+import { Separator as SeparatorPrimitive } from "@base-ui/react/separator";
import { cn } from "../utils";
interface SeparatorProps extends React.ComponentProps {
diff --git a/packages/propel/src/switch/root.tsx b/packages/propel/src/switch/root.tsx
index 0c3e87c2..7dfedd02 100644
--- a/packages/propel/src/switch/root.tsx
+++ b/packages/propel/src/switch/root.tsx
@@ -5,7 +5,7 @@
*/
import * as React from "react";
-import { Switch as BaseSwitch } from "@base-ui-components/react/switch";
+import { Switch as BaseSwitch } from "@base-ui/react/switch";
import { cn } from "../utils/classname";
export interface IToggleSwitchProps {
diff --git a/packages/propel/src/tabs/tabs.tsx b/packages/propel/src/tabs/tabs.tsx
index 56c55a98..13b34445 100644
--- a/packages/propel/src/tabs/tabs.tsx
+++ b/packages/propel/src/tabs/tabs.tsx
@@ -5,7 +5,7 @@
*/
import * as React from "react";
-import { Tabs as TabsPrimitive } from "@base-ui-components/react/tabs";
+import { Tabs as TabsPrimitive } from "@base-ui/react/tabs";
import { cn } from "../utils/classname";
type TabsVariant = "contained";
@@ -93,7 +93,7 @@ const TabsTrigger = React.forwardRef(function TabsTrigger(
data-slot="tabs-trigger"
className={cn(
"flex w-full min-w-fit cursor-pointer items-center justify-center rounded-md border border-transparent p-1 font-medium text-primary transition-all duration-200 ease-in-out outline-none focus:outline-none",
- "data-[selected]:shadow-sm data-[selected]:raised-200 data-[selected]:border data-[selected]:border-subtle-1 data-[selected]:bg-layer-2 data-[selected]:text-primary",
+ "data-[active]:shadow-sm data-[active]:raised-200 data-[active]:border data-[active]:border-subtle-1 data-[active]:bg-layer-2 data-[active]:text-primary",
"text-placeholder hover:bg-layer-transparent-hover hover:text-tertiary",
"disabled:cursor-not-allowed disabled:text-placeholder",
{
diff --git a/packages/propel/src/toast/toast.tsx b/packages/propel/src/toast/toast.tsx
index 9985261f..59d8e699 100644
--- a/packages/propel/src/toast/toast.tsx
+++ b/packages/propel/src/toast/toast.tsx
@@ -5,7 +5,7 @@
*/
import * as React from "react";
-import { Toast as BaseToast } from "@base-ui-components/react/toast";
+import { Toast as BaseToast } from "@base-ui/react/toast";
import { CloseOutline, InfoOutline, TickOutline, WarningTriangleOutline } from "@makeplane/propel/icons";
// spinner
import { CircularBarSpinner } from "../spinners/circular-bar-spinner";
diff --git a/packages/propel/src/tooltip/root.tsx b/packages/propel/src/tooltip/root.tsx
index 19f06fc7..d90bf71b 100644
--- a/packages/propel/src/tooltip/root.tsx
+++ b/packages/propel/src/tooltip/root.tsx
@@ -5,7 +5,7 @@
*/
import * as React from "react";
-import { Tooltip as BaseTooltip } from "@base-ui-components/react/tooltip";
+import { Tooltip as BaseTooltip } from "@base-ui/react/tooltip";
import { cn } from "../utils";
import type { TPlacement, TSide, TAlign } from "../utils/placement";
import { convertPlacementToSideAndAlign } from "../utils/placement";
@@ -52,8 +52,9 @@ export function Tooltip(props: ITooltipProps) {
// No `BaseTooltip.Provider` here on purpose: it exists to share one delay group across many tooltips, so one provider per tooltip is a `FloatingDelayGroup` of one that can never hand off to a neighbour. Mount it once per app root to get the adjacent-instant-open behaviour; the open delay below is passed per tooltip and wins over a provider either way.
return (
-
-
+ // Base UI 1.x owns the hover delays on the trigger, not the root, so that one root can serve several detached triggers with different delays.
+
+ =6.9.0'}
- '@base-ui-components/react@1.0.0-beta.3':
- resolution: {integrity: sha512-4sAq6zmDA9ixV2HRjjeM1+tSEw5R6nvGjXUQmFoQnC3DZLEUdwO94gWDmUDdpoDuChn27jdbaJs9F0Ih4w2UAA==}
- engines: {node: '>=14.0.0'}
- deprecated: Package was renamed to @base-ui/react
- peerDependencies:
- '@types/react': 19.2.17
- react: 19.2.8
- react-dom: 19.2.8
- peerDependenciesMeta:
- '@types/react':
- optional: true
-
- '@base-ui-components/utils@0.1.1':
- resolution: {integrity: sha512-HWXZA8upEKgrdL1rQqxWu1H+2tB2cXzY2jCxvgnpUv3eoWN2jldhXxMZnXIjZF7jahGxSWXfSIM/qskiTWFFxA==}
- deprecated: Package was renamed to @base-ui/utils
- peerDependencies:
- '@types/react': 19.2.17
- react: 19.2.8
- react-dom: 19.2.8
- peerDependenciesMeta:
- '@types/react':
- optional: true
-
'@base-ui/react@1.7.0':
resolution: {integrity: sha512-j+8QjX44C32jrXD/qyEAGpFr70FRpGL2CY61mQd9nBPWN737CK0xxD1ceJ055rW4RtdvFDT1e7otzdlfxvsYug==}
engines: {node: '>=14.0.0'}
@@ -8908,31 +8885,6 @@ snapshots:
'@babel/helper-string-parser': 7.29.7
'@babel/helper-validator-identifier': 7.29.7
- '@base-ui-components/react@1.0.0-beta.3(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
- dependencies:
- '@babel/runtime': 7.29.7
- '@base-ui-components/utils': 0.1.1(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
- '@floating-ui/react-dom': 2.1.9(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
- '@floating-ui/utils': 0.2.12
- react: 19.2.8
- react-dom: 19.2.8(react@19.2.8)
- reselect: 5.3.0
- tabbable: 6.2.0
- use-sync-external-store: 1.6.0(react@19.2.8)
- optionalDependencies:
- '@types/react': 19.2.17
-
- '@base-ui-components/utils@0.1.1(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
- dependencies:
- '@babel/runtime': 7.29.7
- '@floating-ui/utils': 0.2.12
- react: 19.2.8
- react-dom: 19.2.8(react@19.2.8)
- reselect: 5.3.0
- use-sync-external-store: 1.6.0(react@19.2.8)
- optionalDependencies:
- '@types/react': 19.2.17
-
'@base-ui/react@1.7.0(@date-fns/tz@1.4.1)(@types/react@19.2.17)(date-fns@4.1.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
dependencies:
'@babel/runtime': 7.29.7
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 13076179..31292be2 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -8,7 +8,7 @@ catalog:
"@atlaskit/pragmatic-drag-and-drop": "1.7.4"
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "1.4.0"
"@atlaskit/pragmatic-drag-and-drop-hitbox": "1.1.0"
- "@base-ui-components/react": "1.0.0-beta.3"
+ "@base-ui/react": "1.7.0"
"@bprogress/core": "^1.3.4"
"@chromatic-com/storybook": "5.2.1"
"@floating-ui/dom": "^1.7.1"
From 635289de6245d39cd5afcfd0bbe87aa61250e21d Mon Sep 17 00:00:00 2001
From: Evan Hu
Date: Fri, 18 Sep 2026 23:19:31 +0900
Subject: [PATCH 2/5] fix(propel): ship the separator subpath and mark the
package side-effect free
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The build config no longer described the package. `src/separator` existed with a working component and a Storybook story but had no `index.ts`, no tsdown entry and no `exports` key, so `@pace/propel/separator` could not be imported at all — which matters now, because the twelve zero-consumer subpaths are the landing zones the component consolidation is about to use, not dead code. Meanwhile the entry list still named `src/emoji-reaction-picker/index.ts` for a directory that does not exist; tsdown silently ignored it, so the stale entry only served to mislead.
`sideEffects: false` matches the sibling `@pace/ui` manifest and is safe here: nothing under `packages/propel/src` imports CSS or touches module-scope globals, and the one stylesheet the package publishes reaches apps/web and apps/space through a CSS `@import` in their `globals.css`, which bundler side-effect pruning does not see. Without the flag every consumer has to keep all forty-odd subpath modules it touches.
---
packages/propel/package.json | 2 ++
packages/propel/src/separator/index.ts | 7 +++++++
packages/propel/tsdown.config.ts | 2 +-
3 files changed, 10 insertions(+), 1 deletion(-)
create mode 100644 packages/propel/src/separator/index.ts
diff --git a/packages/propel/package.json b/packages/propel/package.json
index f38b5392..4a69e5dd 100644
--- a/packages/propel/package.json
+++ b/packages/propel/package.json
@@ -4,6 +4,7 @@
"private": true,
"license": "AGPL-3.0",
"type": "module",
+ "sideEffects": false,
"exports": {
"./accordion": "./dist/accordion/index.js",
"./animated-counter": "./dist/animated-counter/index.js",
@@ -36,6 +37,7 @@
"./popover": "./dist/popover/index.js",
"./portal": "./dist/portal/index.js",
"./scrollarea": "./dist/scrollarea/index.js",
+ "./separator": "./dist/separator/index.js",
"./skeleton": "./dist/skeleton/index.js",
"./spinners": "./dist/spinners/index.js",
"./switch": "./dist/switch/index.js",
diff --git a/packages/propel/src/separator/index.ts b/packages/propel/src/separator/index.ts
new file mode 100644
index 00000000..b83063bf
--- /dev/null
+++ b/packages/propel/src/separator/index.ts
@@ -0,0 +1,7 @@
+/**
+ * Copyright (c) 2023-present Plane Software, Inc. and contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * See the LICENSE file for details.
+ */
+
+export * from "./separator";
diff --git a/packages/propel/tsdown.config.ts b/packages/propel/tsdown.config.ts
index 5e97cf6f..85d5f525 100644
--- a/packages/propel/tsdown.config.ts
+++ b/packages/propel/tsdown.config.ts
@@ -19,7 +19,6 @@ export default defineConfig({
"src/empty-state/index.ts",
"src/emoji-icon-picker/index.ts",
"src/emoji-reaction/index.ts",
- "src/emoji-reaction-picker/index.ts",
"src/icon-button/index.ts",
"src/icons/index.ts",
"src/input/index.ts",
@@ -28,6 +27,7 @@ export default defineConfig({
"src/popover/index.ts",
"src/portal/index.ts",
"src/scrollarea/index.ts",
+ "src/separator/index.ts",
"src/skeleton/index.ts",
"src/spinners/index.ts",
"src/switch/index.ts",
From 058d5b5aeeb920d3766e73929a8c646051ad61ce Mon Sep 17 00:00:00 2001
From: Evan Hu
Date: Fri, 18 Sep 2026 23:49:56 +0900
Subject: [PATCH 3/5] chore: bump the workspace version, which check-version
requires of every PR
---
apps/admin/package.json | 2 +-
apps/space/package.json | 2 +-
apps/web/package.json | 2 +-
package.json | 2 +-
packages/codemods/package.json | 2 +-
packages/constants/package.json | 2 +-
packages/editor/package.json | 2 +-
packages/hooks/package.json | 2 +-
packages/i18n/package.json | 2 +-
packages/propel/package.json | 2 +-
packages/services/package.json | 2 +-
packages/shared-state/package.json | 2 +-
packages/tailwind-config/package.json | 2 +-
packages/types/package.json | 2 +-
packages/typescript-config/package.json | 2 +-
packages/ui/package.json | 2 +-
packages/utils/package.json | 2 +-
17 files changed, 17 insertions(+), 17 deletions(-)
diff --git a/apps/admin/package.json b/apps/admin/package.json
index e2361a51..64f8472c 100644
--- a/apps/admin/package.json
+++ b/apps/admin/package.json
@@ -1,6 +1,6 @@
{
"name": "admin",
- "version": "1.4.39",
+ "version": "1.4.40",
"private": true,
"description": "Admin UI for Pace",
"license": "AGPL-3.0",
diff --git a/apps/space/package.json b/apps/space/package.json
index 7fe586f1..f0935b01 100644
--- a/apps/space/package.json
+++ b/apps/space/package.json
@@ -1,6 +1,6 @@
{
"name": "space",
- "version": "1.4.39",
+ "version": "1.4.40",
"private": true,
"license": "AGPL-3.0",
"type": "module",
diff --git a/apps/web/package.json b/apps/web/package.json
index cd55c38a..22923ab4 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -1,6 +1,6 @@
{
"name": "web",
- "version": "1.4.39",
+ "version": "1.4.40",
"private": true,
"license": "AGPL-3.0",
"type": "module",
diff --git a/package.json b/package.json
index 6291a891..b95c88b7 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "pace",
- "version": "1.4.39",
+ "version": "1.4.40",
"private": true,
"description": "Project management that keeps pace with the work",
"license": "AGPL-3.0",
diff --git a/packages/codemods/package.json b/packages/codemods/package.json
index e20d849a..4cae1104 100644
--- a/packages/codemods/package.json
+++ b/packages/codemods/package.json
@@ -1,6 +1,6 @@
{
"name": "@pace/codemods",
- "version": "1.4.39",
+ "version": "1.4.40",
"private": true,
"scripts": {
"check:format": "oxfmt --check .",
diff --git a/packages/constants/package.json b/packages/constants/package.json
index b58b4f76..a6bb2ffb 100644
--- a/packages/constants/package.json
+++ b/packages/constants/package.json
@@ -1,6 +1,6 @@
{
"name": "@pace/constants",
- "version": "1.4.39",
+ "version": "1.4.40",
"private": true,
"license": "AGPL-3.0",
"type": "module",
diff --git a/packages/editor/package.json b/packages/editor/package.json
index cc755645..b03cf9c6 100644
--- a/packages/editor/package.json
+++ b/packages/editor/package.json
@@ -1,6 +1,6 @@
{
"name": "@pace/editor",
- "version": "1.4.39",
+ "version": "1.4.40",
"private": true,
"description": "Core Editor that powers Pace",
"keywords": [
diff --git a/packages/hooks/package.json b/packages/hooks/package.json
index a465e16a..17722550 100644
--- a/packages/hooks/package.json
+++ b/packages/hooks/package.json
@@ -1,6 +1,6 @@
{
"name": "@pace/hooks",
- "version": "1.4.39",
+ "version": "1.4.40",
"private": true,
"description": "React hooks that are shared across multiple apps internally",
"license": "AGPL-3.0",
diff --git a/packages/i18n/package.json b/packages/i18n/package.json
index afd8f563..bd192ae2 100644
--- a/packages/i18n/package.json
+++ b/packages/i18n/package.json
@@ -1,6 +1,6 @@
{
"name": "@pace/i18n",
- "version": "1.4.39",
+ "version": "1.4.40",
"private": true,
"description": "I18n shared across multiple apps internally",
"license": "AGPL-3.0",
diff --git a/packages/propel/package.json b/packages/propel/package.json
index 4a69e5dd..be472ef3 100644
--- a/packages/propel/package.json
+++ b/packages/propel/package.json
@@ -1,6 +1,6 @@
{
"name": "@pace/propel",
- "version": "1.4.39",
+ "version": "1.4.40",
"private": true,
"license": "AGPL-3.0",
"type": "module",
diff --git a/packages/services/package.json b/packages/services/package.json
index b2d62b85..3fa4f12d 100644
--- a/packages/services/package.json
+++ b/packages/services/package.json
@@ -1,6 +1,6 @@
{
"name": "@pace/services",
- "version": "1.4.39",
+ "version": "1.4.40",
"private": true,
"license": "AGPL-3.0",
"type": "module",
diff --git a/packages/shared-state/package.json b/packages/shared-state/package.json
index a1f21d63..655d6696 100644
--- a/packages/shared-state/package.json
+++ b/packages/shared-state/package.json
@@ -1,6 +1,6 @@
{
"name": "@pace/shared-state",
- "version": "1.4.39",
+ "version": "1.4.40",
"private": true,
"description": "Shared state shared across multiple apps internally",
"license": "AGPL-3.0",
diff --git a/packages/tailwind-config/package.json b/packages/tailwind-config/package.json
index 4fef73fa..325e18d1 100644
--- a/packages/tailwind-config/package.json
+++ b/packages/tailwind-config/package.json
@@ -1,6 +1,6 @@
{
"name": "@pace/tailwind-config",
- "version": "1.4.39",
+ "version": "1.4.40",
"private": true,
"description": "common tailwind configuration across monorepo",
"license": "AGPL-3.0",
diff --git a/packages/types/package.json b/packages/types/package.json
index f16a1dfd..cb4201ca 100644
--- a/packages/types/package.json
+++ b/packages/types/package.json
@@ -1,6 +1,6 @@
{
"name": "@pace/types",
- "version": "1.4.39",
+ "version": "1.4.40",
"private": true,
"license": "AGPL-3.0",
"type": "module",
diff --git a/packages/typescript-config/package.json b/packages/typescript-config/package.json
index a65311ee..4c0aef1e 100644
--- a/packages/typescript-config/package.json
+++ b/packages/typescript-config/package.json
@@ -1,6 +1,6 @@
{
"name": "@pace/typescript-config",
- "version": "1.4.39",
+ "version": "1.4.40",
"private": true,
"license": "AGPL-3.0",
"files": [
diff --git a/packages/ui/package.json b/packages/ui/package.json
index d3f99689..d639ad76 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -1,6 +1,6 @@
{
"name": "@pace/ui",
- "version": "1.4.39",
+ "version": "1.4.40",
"private": true,
"description": "UI components shared across multiple apps internally",
"license": "AGPL-3.0",
diff --git a/packages/utils/package.json b/packages/utils/package.json
index 84fd1eca..9b0a310c 100644
--- a/packages/utils/package.json
+++ b/packages/utils/package.json
@@ -1,6 +1,6 @@
{
"name": "@pace/utils",
- "version": "1.4.39",
+ "version": "1.4.40",
"private": true,
"description": "Helper functions shared across multiple apps internally",
"license": "AGPL-3.0",
From 51a8a6bd7252bc0cf4135d148d6100ffc0d5d2f8 Mon Sep 17 00:00:00 2001
From: Evan
Date: Sat, 19 Sep 2026 20:25:01 +0800
Subject: [PATCH 4/5] refactor: retire the duplicate @pace/ui components onto
@pace/propel (#259)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* refactor(ui): drop the duplicate Card and point its call sites at @pace/propel
@pace/propel is the library the repo standardises on, and Card is the one place where the two copies were a byte-level duplicate rather than a redesign: packages/propel/src/card/helper.tsx and packages/ui/src/card/helper.tsx were identical, and the two card.tsx files differed on exactly one line — whether `cn` comes from `../utils/classname` or `../utils`. Both are clsx + tailwind-merge, so there is no behaviour to preserve and nothing to decide.
Two of the six consumers (profile/overview/workload.tsx and state-distribution.tsx) imported nothing else from @pace/ui and now import no @pace/ui at all. The rest keep the symbols that have no propel twin, so the specifier is split rather than the whole statement moved.
packages/ui survives as the app-composites layer; only the components that have a propel equivalent leave it.
* chore(codemods): add the @pace/ui Loader to @pace/propel Skeleton rename
The sweep this drives touches 80 files, so the reviewable artefact is the transform and its tests rather than the hunks: the transform has to be provably incapable of changing anything but the import lines and the identifier at its use sites.
It keys off the import source and never off the name. `Loader` is a name app code also defines for itself, and @pace/propel's own emoji picker re-exports a lucide `Loader`; a transform matching `` in JSX would rewrite first-party components in files that import nothing from @pace/ui. The headlessui-v2-default-tags transform already makes that guard and its comment explains why.
It uses the AST only to locate character offsets and splices the replacement into the original text instead of returning `root.toSource()`. An earlier sweep in this effort did go through recast, and recast rewrote unrelated JSX in 7 files — it reprints a whole subtree whenever it cannot reconcile comment attachment, and several of these files carry `// oxlint-disable-next-line` comments inside their JSX. The spec pins that by asserting full output equality on exactly that shape.
Two things were easy to get wrong and are pinned by tests. JSXIdentifier is declared as a subtype of Identifier in ast-types, so searching both types splices every JSX element name twice. And the scope hangs off Program, not off the File node the root path points at — `root.get().scope` is null, so comparing a lookup against it matches every unresolved identifier rather than none, which is the difference between leaving a function-scoped `const Loader` alone and renaming it.
* refactor(ui): retire Loader in favour of @pace/propel's Skeleton across 80 files
Loader and Skeleton are the same component under two names: both wrap their children in `animate-pulse` with `role="status"`, both expose an `.Item` taking `height`/`width`/`className` defaulting to `"auto"`, and the class expressions are identical. Skeleton is the one that lives in the library everything standardises on, and it had zero consumers until now.
Skeleton additionally emits `data-slot` attributes and an `aria-label="Loading content"`, so the rendered markup gains an accessible name it did not have. Nothing styles the old markup — `plane-ui-loader` appeared only on the deleted component's own `displayName`, and the call sites pass only `height`, `width` and `className`, all of which Skeleton supports.
63 of the 80 files imported nothing else from @pace/ui and now import no @pace/ui at all. The rest keep the composites that have no propel twin, so the specifier is split out rather than the statement moved.
Produced by `pnpm --filter @pace/codemods run ui-loader-to-propel-skeleton`; every hunk is an import line or a bare identifier.
* refactor(ui): retire the duplicate Spinner onto @pace/propel/spinners
Both packages exported a `Spinner` with the same name, the same `ISpinner { height = "32px", width = "32px", className }` and the same two SVG paths verbatim. They differed in two places, so propel's copy is corrected first and the 33 call sites move second — that ordering is what makes the swap a no-op rather than a visual change.
The accent fill moves from the arc `` onto the `
)
) : (
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
)}
>
)}
diff --git a/apps/web/core/components/core/modals/bulk-delete-issues-modal.tsx b/apps/web/core/components/core/modals/bulk-delete-issues-modal.tsx
index 49fcecfe..7bdc0a4b 100644
--- a/apps/web/core/components/core/modals/bulk-delete-issues-modal.tsx
+++ b/apps/web/core/components/core/modals/bulk-delete-issues-modal.tsx
@@ -18,7 +18,8 @@ import { SearchOutline } from "@makeplane/propel/icons";
import { TOAST_TYPE, setToast } from "@pace/propel/toast";
import type { ISearchIssueResponse, IUser } from "@pace/types";
import { EIssuesStoreType } from "@pace/types";
-import { Loader, EModalPosition, EModalWidth, ModalCore } from "@pace/ui";
+import { Skeleton } from "@pace/propel/skeleton";
+import { EModalPosition, EModalWidth, ModalCore } from "@pace/ui";
// assets
import darkIssuesAsset from "@/app/assets/empty-state/search/issues-dark.webp?url";
import lightIssuesAsset from "@/app/assets/empty-state/search/issues-light.webp?url";
@@ -185,12 +186,12 @@ export const BulkDeleteIssuesModal = observer(function BulkDeleteIssuesModal(pro
{isSearching ? (
-
-
-
-
-
-
+
+
+
+
+
+
) : (
<>{issueList}>
)}
diff --git a/apps/web/core/components/core/modals/existing-issues-list-modal.tsx b/apps/web/core/components/core/modals/existing-issues-list-modal.tsx
index 7a5bf364..f1a7124c 100644
--- a/apps/web/core/components/core/modals/existing-issues-list-modal.tsx
+++ b/apps/web/core/components/core/modals/existing-issues-list-modal.tsx
@@ -16,7 +16,8 @@ import { Tooltip } from "@makeplane/propel/components/tooltip";
import type { ISearchIssueResponse, TProjectIssuesSearchParams } from "@pace/types";
// ui
import { Switch } from "@makeplane/propel/components/switch";
-import { Loader, EModalPosition, EModalWidth, ModalCore } from "@pace/ui";
+import { Skeleton } from "@pace/propel/skeleton";
+import { EModalPosition, EModalWidth, ModalCore } from "@pace/ui";
import { generateWorkItemLink, getTabIndex } from "@pace/utils";
// helpers
// hooks
@@ -235,12 +236,12 @@ export function ExistingIssuesListModal(props: Props) {
)}
{isSearching || isLoading ? (
-
-
-
-
-
-
+
+
+
+
+
+
) : (
<>
{filteredIssues.length === 0 ? (
diff --git a/apps/web/core/components/cycles/active-cycle/cycle-stats.tsx b/apps/web/core/components/cycles/active-cycle/cycle-stats.tsx
index ecb851db..ad1f5dfc 100644
--- a/apps/web/core/components/cycles/active-cycle/cycle-stats.tsx
+++ b/apps/web/core/components/cycles/active-cycle/cycle-stats.tsx
@@ -20,7 +20,7 @@ import type { TWorkItemFilterCondition } from "@pace/shared-state";
import type { ICycle } from "@pace/types";
import { EIssuesStoreType } from "@pace/types";
// ui
-import { Loader } from "@pace/ui";
+import { Skeleton } from "@pace/propel/skeleton";
import { cn, renderFormattedDate, renderFormattedDateWithoutYear, getFileURL } from "@pace/utils";
// assets
import darkAssigneeAsset from "@/app/assets/empty-state/active-cycle/assignee-dark.webp?url";
@@ -97,11 +97,11 @@ export const ActiveCycleStats = observer(function ActiveCycleStats(props: Active
useIntersectionObserver(issuesContainerRef, issuesLoaderElement, loadMoreIssues, `0% 0% 100% 0%`);
const loaders = (
-
-
-
-
-
+
+
+
+
+
);
return cycleId ? (
@@ -370,8 +370,8 @@ export const ActiveCycleStats = observer(function ActiveCycleStats(props: Active
) : (
-
-
-
+
+
+
);
});
diff --git a/apps/web/core/components/cycles/active-cycle/productivity.tsx b/apps/web/core/components/cycles/active-cycle/productivity.tsx
index 57e6a81f..cabb1619 100644
--- a/apps/web/core/components/cycles/active-cycle/productivity.tsx
+++ b/apps/web/core/components/cycles/active-cycle/productivity.tsx
@@ -11,7 +11,7 @@ import { useTheme } from "next-themes";
// pace imports
import { useTranslation } from "@pace/i18n";
import type { ICycle, TCycleEstimateType } from "@pace/types";
-import { Loader } from "@pace/ui";
+import { Skeleton } from "@pace/propel/skeleton";
// assets
import darkChartAsset from "@/app/assets/empty-state/active-cycle/chart-dark.webp?url";
import lightChartAsset from "@/app/assets/empty-state/active-cycle/chart-light.webp?url";
@@ -101,8 +101,8 @@ export const ActiveCycleProductivity = observer(function ActiveCycleProductivity
) : (
-
-
-
+
+
+
);
});
diff --git a/apps/web/core/components/cycles/active-cycle/progress.tsx b/apps/web/core/components/cycles/active-cycle/progress.tsx
index f4d97b61..b54e5c2d 100644
--- a/apps/web/core/components/cycles/active-cycle/progress.tsx
+++ b/apps/web/core/components/cycles/active-cycle/progress.tsx
@@ -12,7 +12,7 @@ import { useTranslation } from "@pace/i18n";
import type { TWorkItemFilterCondition } from "@pace/shared-state";
import { LinearProgress } from "@makeplane/propel/components/linear-progress";
import type { ICycle } from "@pace/types";
-import { Loader } from "@pace/ui";
+import { Skeleton } from "@pace/propel/skeleton";
// assets
import darkProgressAsset from "@/app/assets/empty-state/active-cycle/progress-dark.webp?url";
import lightProgressAsset from "@/app/assets/empty-state/active-cycle/progress-light.webp?url";
@@ -116,8 +116,8 @@ export const ActiveCycleProgress = observer(function ActiveCycleProgress(props:
)}
) : (
-
-
-
+
+
+
);
});
diff --git a/apps/web/core/components/cycles/analytics-sidebar/root.tsx b/apps/web/core/components/cycles/analytics-sidebar/root.tsx
index 30253e09..77712ccd 100644
--- a/apps/web/core/components/cycles/analytics-sidebar/root.tsx
+++ b/apps/web/core/components/cycles/analytics-sidebar/root.tsx
@@ -7,7 +7,7 @@
import React from "react";
import { observer } from "mobx-react";
// pace imports
-import { Loader } from "@pace/ui";
+import { Skeleton } from "@pace/propel/skeleton";
// local imports
import useCyclesDetails from "../active-cycle/use-cycles-details";
import { CycleAnalyticsProgress } from "./issue-progress";
@@ -34,17 +34,17 @@ export const CycleDetailsSidebar = observer(function CycleDetailsSidebar(props:
if (!cycleDetails)
return (
-
+
-
-
+
+
-
-
-
+
+
+
-
+
);
return (
diff --git a/apps/web/core/components/cycles/analytics-sidebar/sidebar-chart.tsx b/apps/web/core/components/cycles/analytics-sidebar/sidebar-chart.tsx
index b8d35190..cde79d59 100644
--- a/apps/web/core/components/cycles/analytics-sidebar/sidebar-chart.tsx
+++ b/apps/web/core/components/cycles/analytics-sidebar/sidebar-chart.tsx
@@ -9,7 +9,7 @@ import { observer } from "mobx-react";
// pace imports
import { useTranslation } from "@pace/i18n";
import type { TCycleEstimateType } from "@pace/types";
-import { Loader } from "@pace/ui";
+import { Skeleton } from "@pace/propel/skeleton";
import { getDate } from "@pace/utils";
// components
import ProgressChart from "@/components/core/sidebar/progress-chart";
@@ -79,9 +79,9 @@ export const SidebarChart = observer(function SidebarChart(props: ProgressChartP
/>
) : (
-
-
-
+
+
+
)}
diff --git a/apps/web/core/components/dropdowns/intake-state/base.tsx b/apps/web/core/components/dropdowns/intake-state/base.tsx
index 6f936f04..ca8d2e4c 100644
--- a/apps/web/core/components/dropdowns/intake-state/base.tsx
+++ b/apps/web/core/components/dropdowns/intake-state/base.tsx
@@ -14,7 +14,8 @@ import { useTranslation } from "@pace/i18n";
import { IntakeStateGroupIcon } from "@pace/propel/icons";
import { ChevronDownOutline, SearchOutline } from "@makeplane/propel/icons";
import type { IIntakeState } from "@pace/types";
-import { ComboDropDown, Spinner } from "@pace/ui";
+import { Spinner } from "@pace/propel/spinners";
+import { ComboDropDown } from "@pace/ui";
import { cn } from "@pace/utils";
// components
import { DropdownButton } from "@/components/dropdowns/buttons";
diff --git a/apps/web/core/components/dropdowns/state/base.tsx b/apps/web/core/components/dropdowns/state/base.tsx
index 6889263d..747a4b48 100644
--- a/apps/web/core/components/dropdowns/state/base.tsx
+++ b/apps/web/core/components/dropdowns/state/base.tsx
@@ -14,7 +14,8 @@ import { useTranslation } from "@pace/i18n";
import { StateGroupIcon } from "@pace/propel/icons";
import { ChevronDownOutline, SearchOutline } from "@makeplane/propel/icons";
import type { IState } from "@pace/types";
-import { ComboDropDown, Spinner } from "@pace/ui";
+import { Spinner } from "@pace/propel/spinners";
+import { ComboDropDown } from "@pace/ui";
import { cn } from "@pace/utils";
// components
import { DropdownButton } from "@/components/dropdowns/buttons";
diff --git a/apps/web/core/components/editor/rich-text/description-input/loader.tsx b/apps/web/core/components/editor/rich-text/description-input/loader.tsx
index 7651dadf..5bddc4d0 100644
--- a/apps/web/core/components/editor/rich-text/description-input/loader.tsx
+++ b/apps/web/core/components/editor/rich-text/description-input/loader.tsx
@@ -5,7 +5,7 @@
*/
// pace imports
-import { Loader } from "@pace/ui";
+import { Skeleton } from "@pace/propel/skeleton";
import { cn } from "@pace/utils";
type Props = {
@@ -16,24 +16,24 @@ export function DescriptionInputLoader(props: Props) {
const { className } = props;
return (
-
-
+
+
-
-
+
+
-
-
+
+
-
+
-
+
-
-
+
+
-
+
);
}
diff --git a/apps/web/core/components/estimates/loader-screen.tsx b/apps/web/core/components/estimates/loader-screen.tsx
index 15453c82..393d3236 100644
--- a/apps/web/core/components/estimates/loader-screen.tsx
+++ b/apps/web/core/components/estimates/loader-screen.tsx
@@ -4,15 +4,15 @@
* See the LICENSE file for details.
*/
-import { Loader } from "@pace/ui";
+import { Skeleton } from "@pace/propel/skeleton";
export function EstimateLoaderScreen() {
return (
-
-
-
-
-
-
+
+
+
+
+
+
);
}
diff --git a/apps/web/core/components/estimates/points/create.tsx b/apps/web/core/components/estimates/points/create.tsx
index 953be1e1..5f4414a1 100644
--- a/apps/web/core/components/estimates/points/create.tsx
+++ b/apps/web/core/components/estimates/points/create.tsx
@@ -13,7 +13,7 @@ import { useTranslation } from "@pace/i18n";
import { TOAST_TYPE, setToast } from "@pace/propel/toast";
import { Tooltip } from "@makeplane/propel/components/tooltip";
import type { TEstimatePointsObject, TEstimateSystemKeys, TEstimateTypeErrorObject } from "@pace/types";
-import { Spinner } from "@pace/ui";
+import { Spinner } from "@pace/propel/spinners";
import { cn, isEstimatePointValuesRepeated } from "@pace/utils";
import { EstimateInputRoot } from "@/components/estimates/inputs/root";
// helpers
diff --git a/apps/web/core/components/estimates/points/update.tsx b/apps/web/core/components/estimates/points/update.tsx
index 2c750be5..c57b7359 100644
--- a/apps/web/core/components/estimates/points/update.tsx
+++ b/apps/web/core/components/estimates/points/update.tsx
@@ -13,7 +13,7 @@ import { useTranslation } from "@pace/i18n";
import { TOAST_TYPE, setToast } from "@pace/propel/toast";
import { Tooltip } from "@makeplane/propel/components/tooltip";
import type { TEstimatePointsObject, TEstimateSystemKeys, TEstimateTypeErrorObject } from "@pace/types";
-import { Spinner } from "@pace/ui";
+import { Spinner } from "@pace/propel/spinners";
import { cn, isEstimatePointValuesRepeated } from "@pace/utils";
import { EstimateInputRoot } from "@/components/estimates/inputs/root";
// helpers
diff --git a/apps/web/core/components/gantt-chart/sidebar/issues/sidebar.tsx b/apps/web/core/components/gantt-chart/sidebar/issues/sidebar.tsx
index 639ee7d2..7e1a6850 100644
--- a/apps/web/core/components/gantt-chart/sidebar/issues/sidebar.tsx
+++ b/apps/web/core/components/gantt-chart/sidebar/issues/sidebar.tsx
@@ -10,7 +10,7 @@ import { observer } from "mobx-react";
// ui
import { GANTT_TIMELINE_TYPE } from "@pace/types";
import type { IBlockUpdateData } from "@pace/types";
-import { Loader } from "@pace/ui";
+import { Skeleton } from "@pace/propel/skeleton";
// components
import RenderIfVisible from "@/components/core/render-if-visible-HOC";
import { GanttLayoutListItemLoader } from "@/components/ui/loader/layouts/gantt-layout-loader";
@@ -122,12 +122,12 @@ export const IssueGanttSidebar = observer(function IssueGanttSidebar(props: Prop
)}
>
) : (
-
-
-
-
-
-
+
+
+
+
+
+
)}
);
diff --git a/apps/web/core/components/gantt-chart/sidebar/modules/sidebar.tsx b/apps/web/core/components/gantt-chart/sidebar/modules/sidebar.tsx
index 2048b17a..fae2bc59 100644
--- a/apps/web/core/components/gantt-chart/sidebar/modules/sidebar.tsx
+++ b/apps/web/core/components/gantt-chart/sidebar/modules/sidebar.tsx
@@ -8,7 +8,7 @@ import { observer } from "mobx-react";
// ui
import { GANTT_TIMELINE_TYPE } from "@pace/types";
import type { IBlockUpdateData } from "@pace/types";
-import { Loader } from "@pace/ui";
+import { Skeleton } from "@pace/propel/skeleton";
// components
// hooks
import { useTimeLineChart } from "@/hooks/use-timeline-chart";
@@ -52,12 +52,12 @@ export const ModuleGanttSidebar = observer(function ModuleGanttSidebar(props: Pr
))
) : (
-
-
-
-
-
-
+
+
+
+
+
+
)}
);
diff --git a/apps/web/core/components/global/product-updates/changelog.tsx b/apps/web/core/components/global/product-updates/changelog.tsx
index e9f565ad..d3dfa7c1 100644
--- a/apps/web/core/components/global/product-updates/changelog.tsx
+++ b/apps/web/core/components/global/product-updates/changelog.tsx
@@ -7,7 +7,7 @@
import { useState, useEffect, useRef } from "react";
import { observer } from "mobx-react";
// hooks
-import { Loader } from "@pace/ui";
+import { Skeleton } from "@pace/propel/skeleton";
import { ProductUpdatesFallback } from "@/components/global/product-updates/fallback";
import { useInstance } from "@/hooks/store/use-instance";
@@ -74,9 +74,9 @@ export const ProductUpdatesChangelog = observer(function ProductUpdatesChangelog
return (
);
diff --git a/apps/web/core/components/ui/loader/layouts/project-inbox/inbox-layout-loader.tsx b/apps/web/core/components/ui/loader/layouts/project-inbox/inbox-layout-loader.tsx
index cdca18b8..c38a06b6 100644
--- a/apps/web/core/components/ui/loader/layouts/project-inbox/inbox-layout-loader.tsx
+++ b/apps/web/core/components/ui/loader/layouts/project-inbox/inbox-layout-loader.tsx
@@ -6,7 +6,7 @@
import React from "react";
// ui
-import { Loader } from "@pace/ui";
+import { Skeleton } from "@pace/propel/skeleton";
import { InboxSidebarLoader } from "./inbox-sidebar-loader";
export function InboxLayoutLoader() {
@@ -16,15 +16,15 @@ export function InboxLayoutLoader() {
-
+
-
-
-
-
+
+
+
+
-
-
+
+
);
diff --git a/apps/web/core/components/workspace-notifications/sidebar/header/options/root.tsx b/apps/web/core/components/workspace-notifications/sidebar/header/options/root.tsx
index 74692f35..4afcf2ec 100644
--- a/apps/web/core/components/workspace-notifications/sidebar/header/options/root.tsx
+++ b/apps/web/core/components/workspace-notifications/sidebar/header/options/root.tsx
@@ -10,7 +10,7 @@ import { CheckDoneOutline, RefreshOutline } from "@makeplane/propel/icons";
import { ENotificationLoader, ENotificationQueryParamType } from "@pace/constants";
import { useTranslation } from "@pace/i18n";
import { Tooltip } from "@makeplane/propel/components/tooltip";
-import { Spinner } from "@pace/ui";
+import { Spinner } from "@pace/propel/spinners";
// hooks
import { useWorkspaceNotifications } from "@/hooks/store/notifications";
import { usePlatformOS } from "@/hooks/use-platform-os";
diff --git a/apps/web/core/components/workspace/sidebar/projects-list.tsx b/apps/web/core/components/workspace/sidebar/projects-list.tsx
index 5680a2eb..8dac3486 100644
--- a/apps/web/core/components/workspace/sidebar/projects-list.tsx
+++ b/apps/web/core/components/workspace/sidebar/projects-list.tsx
@@ -18,7 +18,7 @@ import { useTranslation } from "@pace/i18n";
import { IconButton } from "@pace/propel/icon-button";
import { TOAST_TYPE, setToast } from "@pace/propel/toast";
import { Tooltip } from "@makeplane/propel/components/tooltip";
-import { Loader } from "@pace/ui";
+import { Skeleton } from "@pace/propel/skeleton";
import { copyUrlToClipboard, cn, orderJoinedProjects } from "@pace/utils";
// components
import { CreateProjectModal } from "@/components/project/create-project-modal";
@@ -228,11 +228,11 @@ export const SidebarProjectsList = observer(function SidebarProjectsList() {
leaveTo="transform scale-95 opacity-0"
>
{loader === "init-loader" && (
-
+
{Array.from({ length: 4 }).map((_, index) => (
-
+
))}
-
+
)}
{isAllProjectsListOpen && (
diff --git a/apps/web/core/components/workspace/sidebar/workspace-menu-root.tsx b/apps/web/core/components/workspace/sidebar/workspace-menu-root.tsx
index d3214d6b..ea9c67e9 100644
--- a/apps/web/core/components/workspace/sidebar/workspace-menu-root.tsx
+++ b/apps/web/core/components/workspace/sidebar/workspace-menu-root.tsx
@@ -15,7 +15,7 @@ import { Menu, Transition } from "@headlessui/react";
import { useTranslation } from "@pace/i18n";
import { TOAST_TYPE, setToast } from "@pace/propel/toast";
import type { IWorkspace } from "@pace/types";
-import { Loader } from "@pace/ui";
+import { Skeleton } from "@pace/propel/skeleton";
import { orderWorkspacesList, cn } from "@pace/utils";
// helpers
import { AppSidebarItem } from "@/components/sidebar/sidebar-item";
@@ -178,10 +178,10 @@ export const WorkspaceMenuRoot = observer(function WorkspaceMenuRoot(props: Work
) : (
-
-
-
-
+
+
+
+
)}
diff --git a/packages/codemods/tests/ui-loader-to-propel-skeleton.spec.ts b/packages/codemods/tests/ui-loader-to-propel-skeleton.spec.ts
new file mode 100644
index 00000000..619e8985
--- /dev/null
+++ b/packages/codemods/tests/ui-loader-to-propel-skeleton.spec.ts
@@ -0,0 +1,183 @@
+/**
+ * Copyright (c) 2023-present Plane Software, Inc. and contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * See the LICENSE file for details.
+ */
+
+import { describe, it, expect } from "vitest";
+import { applyTransform } from "@hypermod/utils";
+import * as transformer from "../ui-loader-to-propel-skeleton";
+
+const apply = (source: string) =>
+ applyTransform(transformer, source, { parser: "tsx" });
+
+describe("ui-loader-to-propel-skeleton", () => {
+ it("moves a sole-specifier import and renames the element and its .Item", async () => {
+ const result = await apply(`
+ import { Loader } from "@pace/ui";
+
+ export const Placeholder = () => (
+
+
+
+ );
+ `);
+
+ expect(result).toContain(
+ `import { Skeleton } from "@pace/propel/skeleton";`
+ );
+ expect(result).not.toContain("@pace/ui");
+ expect(result).toContain(``);
+ expect(result).toContain(``);
+ expect(result).toContain("");
+ });
+
+ it("splits Loader out and leaves the other @pace/ui specifiers in place", async () => {
+ const result = await apply(`
+ import { Loader, CustomMenu, Row } from "@pace/ui";
+
+ export const Placeholder = () => ;
+ `);
+
+ expect(result).toContain(
+ `import { Skeleton } from "@pace/propel/skeleton";`
+ );
+ expect(result).toContain(`import { CustomMenu, Row } from "@pace/ui";`);
+ });
+
+ it("removes a trailing Loader specifier without leaving a dangling comma", async () => {
+ const result = await apply(`
+ import { CustomMenu, Loader } from "@pace/ui";
+
+ export const Placeholder = () => ;
+ `);
+
+ expect(result).toContain(`import { CustomMenu } from "@pace/ui";`);
+ expect(result).not.toMatch(/,\s*\}/);
+ });
+
+ it("keeps the leading comment above both statements", async () => {
+ const result = await apply(`
+ // ui
+ import { Loader, Row } from "@pace/ui";
+
+ export const Placeholder = () => ;
+ `);
+
+ expect(result).toMatch(
+ /\/\/ ui\n\s*import \{ Skeleton \} from "@pace\/propel\/skeleton";\n\s*import \{ Row \} from "@pace\/ui";/
+ );
+ });
+
+ it("handles a multi-line specifier list", async () => {
+ const result = await apply(`
+ import {
+ CustomMenu,
+ Loader,
+ Row,
+ } from "@pace/ui";
+
+ export const Placeholder = () => ;
+ `);
+
+ expect(result).toContain("CustomMenu,");
+ expect(result).toContain("Row,");
+ expect(result).not.toMatch(/\bLoader\b/);
+ });
+
+ it("leaves a same-named local component alone when nothing comes from @pace/ui", async () => {
+ const result = await apply(`
+ import { Row } from "@pace/ui";
+
+ function Loader() {
+ return ;
+ }
+
+ export const Placeholder = () => ;
+ `);
+
+ expect(result).not.toContain("@pace/propel/skeleton");
+ expect(result).toContain("function Loader()");
+ expect(result).toContain("");
+ });
+
+ it("leaves a file that imports no UI library untouched", async () => {
+ const result = await apply(`
+ import { Loader } from "@/components/loader";
+
+ export const Placeholder = () => ;
+ `);
+
+ expect(result).not.toContain("@pace/propel/skeleton");
+ expect(result).toContain(`import { Loader } from "@/components/loader";`);
+ });
+
+ it("does not rename a reference shadowed inside a function", async () => {
+ const result = await apply(`
+ import { Loader } from "@pace/ui";
+
+ export function render() {
+ const Loader = () => ;
+ return ;
+ }
+ `);
+
+ expect(result).toContain(
+ `import { Skeleton } from "@pace/propel/skeleton";`
+ );
+ expect(result).toContain("const Loader = () => ;");
+ expect(result).toContain("return ;");
+ });
+
+ it("keeps an aliased local name and renames nothing", async () => {
+ const result = await apply(`
+ import { Loader as UiLoader } from "@pace/ui";
+
+ export const Placeholder = () => ;
+ `);
+
+ expect(result).toContain(
+ `import { Skeleton as UiLoader } from "@pace/propel/skeleton";`
+ );
+ expect(result).toContain("");
+ });
+
+ it("preserves the type-only modifier", async () => {
+ const result = await apply(`
+ import type { Loader } from "@pace/ui";
+
+ export type Props = { loader: typeof Loader };
+ `);
+
+ expect(result).toContain(
+ `import type { Skeleton } from "@pace/propel/skeleton";`
+ );
+ expect(result).toContain("typeof Skeleton");
+ });
+
+ it("reprints nothing but the lines it edits, JSX comments included", async () => {
+ // This is the shape recast mangled on an earlier sweep: an oxlint directive inside the JSX, a
+ // self-closing element next to a text node, and blank lines between siblings.
+ const source = `import { Loader } from "@pace/ui";
+
+export const Placeholder = () => (
+
+ {/* oxlint-disable-next-line jsx-a11y/no-static-element-interactions */}
+
+ label
+
+
+
+
+);
+`;
+
+ // applyTransform trims what it returns, hence the trim on the expectation rather than on the input.
+ expect(await apply(source)).toBe(
+ source
+ .replaceAll("Loader", "Skeleton")
+ .replace(`from "@pace/ui"`, `from "@pace/propel/skeleton"`)
+ .trim()
+ );
+ });
+});
diff --git a/packages/codemods/ui-loader-to-propel-skeleton.ts b/packages/codemods/ui-loader-to-propel-skeleton.ts
new file mode 100644
index 00000000..ccc903fa
--- /dev/null
+++ b/packages/codemods/ui-loader-to-propel-skeleton.ts
@@ -0,0 +1,152 @@
+/**
+ * Copyright (c) 2023-present Plane Software, Inc. and contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * See the LICENSE file for details.
+ */
+
+import type {
+ API,
+ ASTPath,
+ FileInfo,
+ ImportDeclaration,
+ ImportSpecifier,
+} from "jscodeshift";
+
+/**
+ * `Loader` from `@pace/ui` -> `Skeleton` from `@pace/propel/skeleton`.
+ *
+ * The two are the same component under two names: both render `animate-pulse` + `role="status"` around their children and expose an `.Item` taking `height`/`width`/`className` with an `"auto"` default, and the class expressions are identical. Skeleton additionally sets `data-slot` and an `aria-label`, which is additive — no selector in the repo targets the old markup (`plane-ui-loader` appears only on the deleted component's own `displayName`).
+ *
+ * Everything keys off the import source, never off the name. `Loader` is a name several app files define for themselves, and one of them (`packages/editor/src/components/editors/document/loader.tsx`) both defines a `DocumentContentLoader` and imports the @pace/ui `Loader`. A transform that matched `` in JSX would rewrite first-party components in the files that never imported anything. The headlessui-v2-default-tags transform makes the same guard for the same reason.
+ *
+ * The AST is used only to locate character offsets; the replacement is spliced into the original text and the source string is returned instead of `root.toSource()`. Reprinting a mutated AST through recast is not byte-preserving — on a 531-file sweep in this repo it rewrote unrelated JSX in 7 files, because recast reprints a whole subtree when it cannot reconcile comment attachment and several files carry `// oxlint-disable-next-line` comments inside their JSX. Splicing keeps every hunk in the 80 files to the import lines and the bare identifier renames.
+ *
+ * Deliberately not done:
+ *
+ * - No merging into an existing `@pace/propel/skeleton` import, and no reordering. A file keeping other @pace/ui symbols gains a second import statement above the one it had; that is what the icons sweep does too, and both oxlint and oxfmt accept it.
+ * - An aliased import (`Loader as X`) keeps its local name — the new import becomes `Skeleton as X` and no reference is touched. There is none in the repo today, but renaming references would otherwise have to prove the alias is free at every use site.
+ * - A reference whose scope resolves to something other than the module-level import is left alone, which is what makes a shadowing `const Loader` inside a function safe.
+ */
+const FROM_SOURCE = "@pace/ui";
+const TO_SOURCE = "@pace/propel/skeleton";
+const FROM_NAME = "Loader";
+const TO_NAME = "Skeleton";
+
+type Splice = { start: number; end: number; text: string };
+
+/** Babel records these; ast-types does not declare them on the node types. */
+type Ranged = { start?: number; end?: number };
+
+const rangeOf = (node: unknown): [number, number] | undefined => {
+ const { start, end } = (node ?? {}) as Ranged;
+ return typeof start === "number" && typeof end === "number"
+ ? [start, end]
+ : undefined;
+};
+
+export default function transform(file: FileInfo, api: API) {
+ const j = api.jscodeshift;
+ const root = j(file.source);
+
+ const splices: Splice[] = [];
+ /** Local names bound to @pace/ui's `Loader` that are safe to rename at their use sites. */
+ const renamedLocals = new Set();
+
+ root
+ .find(j.ImportDeclaration, { source: { value: FROM_SOURCE } })
+ .forEach((path: ASTPath) => {
+ const specifiers = path.node.specifiers ?? [];
+ const index = specifiers.findIndex(
+ (specifier) =>
+ specifier.type === "ImportSpecifier" &&
+ // ast-types types both names as `string | IdentifierKind`; only the string form can be compared.
+ typeof specifier.imported?.name === "string" &&
+ specifier.imported.name === FROM_NAME
+ );
+ if (index === -1) return;
+
+ const specifier = specifiers[index] as ImportSpecifier;
+ const specifierRange = rangeOf(specifier);
+ const declarationRange = rangeOf(path.node);
+ if (!specifierRange || !declarationRange) return;
+
+ const localName =
+ typeof specifier.local?.name === "string"
+ ? specifier.local.name
+ : FROM_NAME;
+ // An alias only has to survive when it differs from both names; `Loader as Skeleton` already binds the target name.
+ const binding =
+ localName === FROM_NAME || localName === TO_NAME
+ ? TO_NAME
+ : `${TO_NAME} as ${localName}`;
+ // `import type { Loader }` binds no runtime value; the replacement has to keep the modifier or the emitted import stops type-checking.
+ const keyword =
+ path.node.importKind === "type" ? "import type" : "import";
+ const declaration = `${keyword} { ${binding} } from "${TO_SOURCE}";`;
+
+ if (specifiers.length === 1) {
+ // Nothing else came from @pace/ui, so the statement itself moves and the file loses the dependency.
+ splices.push({
+ start: declarationRange[0],
+ end: declarationRange[1],
+ text: declaration,
+ });
+ } else {
+ // Cutting to the next specifier's start (or back from the previous one's end when this is the last) takes the separating comma and whitespace with it, whatever the formatting.
+ const neighbour =
+ index + 1 < specifiers.length
+ ? rangeOf(specifiers[index + 1])
+ : rangeOf(specifiers[index - 1]);
+ if (!neighbour) return;
+ const [cutStart, cutEnd] =
+ index + 1 < specifiers.length
+ ? [specifierRange[0], neighbour[0]]
+ : [neighbour[1], specifierRange[1]];
+ splices.push({ start: cutStart, end: cutEnd, text: "" });
+ // Inserted at the `import` keyword, so a leading `// ui` style comment stays above both statements.
+ splices.push({
+ start: declarationRange[0],
+ end: declarationRange[0],
+ text: `${declaration}\n`,
+ });
+ }
+
+ if (localName === FROM_NAME) renamedLocals.add(localName);
+ });
+
+ if (splices.length === 0) return file.source;
+
+ // The scope hangs off Program, not off the File node the root path points at — `root.get().scope` is null, and comparing against null would have matched every unresolved identifier instead of none.
+ const moduleScope = root.find(j.Program).paths()[0]?.scope;
+
+ if (renamedLocals.size > 0 && moduleScope) {
+ const rename = (path: ASTPath<{ name?: unknown }>) => {
+ const name = path.node.name;
+ if (typeof name !== "string" || !renamedLocals.has(name)) return;
+ // The import specifier's own identifiers sit inside a range already being replaced.
+ if (path.parent?.node?.type === "ImportSpecifier") return;
+ // A JSX member expression's property (`Loader.Item` -> the `Item`) must not be touched.
+ if (
+ path.parent?.node?.type === "JSXMemberExpression" &&
+ path.parent.node.property === path.node
+ )
+ return;
+ if (path.scope?.lookup(name) !== moduleScope) return;
+ const range = rangeOf(path.node);
+ if (range)
+ splices.push({ start: range[0], end: range[1], text: TO_NAME });
+ };
+
+ // One pass, not two: ast-types declares JSXIdentifier as a subtype of Identifier, so this collection already contains the JSX element names. Searching both types would splice every one of them twice.
+ root.find(j.Identifier).forEach(rename);
+ }
+
+ // Back to front, so an earlier splice never invalidates a later offset.
+ let output = file.source;
+ for (const splice of splices.toSorted((a, b) => b.start - a.start)) {
+ output =
+ output.slice(0, splice.start) + splice.text + output.slice(splice.end);
+ }
+
+ return output;
+}
diff --git a/packages/editor/src/components/editors/document/loader.tsx b/packages/editor/src/components/editors/document/loader.tsx
index a37dddf8..61461027 100644
--- a/packages/editor/src/components/editors/document/loader.tsx
+++ b/packages/editor/src/components/editors/document/loader.tsx
@@ -5,7 +5,7 @@
*/
// pace imports
-import { Loader } from "@pace/ui";
+import { Skeleton } from "@pace/propel/skeleton";
import { cn } from "@pace/utils";
type Props = {
@@ -17,41 +17,41 @@ export function DocumentContentLoader(props: Props) {
return (
-
+
-
+
-
+
-
-
+
+
-
+
-
-
+
+
-
-
+
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
);
}
diff --git a/packages/ui/src/avatar/avatar-group.tsx b/packages/propel/src/avatar/avatar-group.tsx
similarity index 94%
rename from packages/ui/src/avatar/avatar-group.tsx
rename to packages/propel/src/avatar/avatar-group.tsx
index 2b8162b5..edd416b5 100644
--- a/packages/ui/src/avatar/avatar-group.tsx
+++ b/packages/propel/src/avatar/avatar-group.tsx
@@ -6,12 +6,12 @@
import React from "react";
// ui
-import { Tooltip } from "@pace/propel/tooltip";
+import { Tooltip } from "../tooltip";
// helpers
import { cn } from "../utils";
// types
-import type { TAvatarSize } from "./helper";
-import { getSizeInfo, isAValidNumber } from "./helper";
+import type { TAvatarSize } from "./avatar";
+import { getSizeInfo, isAValidNumber } from "./avatar";
type Props = {
/**
diff --git a/packages/propel/src/avatar/avatar.tsx b/packages/propel/src/avatar/avatar.tsx
index e4ce720f..dc81d690 100644
--- a/packages/propel/src/avatar/avatar.tsx
+++ b/packages/propel/src/avatar/avatar.tsx
@@ -6,6 +6,7 @@
import React from "react";
import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar";
+import { Tooltip } from "../tooltip";
import { cn } from "../utils/classname";
export type TAvatarSize = "sm" | "md" | "base" | "lg" | number;
@@ -15,7 +16,7 @@ type Props = {
fallbackBackgroundColor?: string; //The background color if the avatar image fails to load
fallbackText?: string;
fallbackTextColor?: string; //The text color if the avatar image fails to load
- showTooltip?: boolean;
+ showTooltip?: boolean; //Whether the name is surfaced as a hover tooltip. @default true
size?: TAvatarSize; //The size of the avatars
shape?: "circle" | "square";
src?: string; //The source of the avatar image
@@ -91,6 +92,7 @@ export function Avatar(props: Props) {
fallbackBackgroundColor,
fallbackText,
fallbackTextColor,
+ showTooltip = true,
size = "md",
shape = "circle",
src,
@@ -102,32 +104,41 @@ export function Avatar(props: Props) {
const fallbackLetter = name?.[0]?.toUpperCase() ?? fallbackText ?? "?";
return (
-
-
-
-
- {fallbackLetter}
-
-
-
+ // The tooltip is how an avatar says whose it is: the label is the only text the component renders beyond a single initial. `disabled` rather than a conditional wrapper keeps the DOM identical either way, so turning the tooltip off cannot reflow a row of avatars.
+
+
+
+ {/* `alt` carries the name for anyone not hovering: the tooltip is pointer-only, and an avatar image with no alt text reads as its URL. */}
+
+
+ {fallbackLetter}
+
+
+
+
);
}
diff --git a/packages/propel/src/avatar/index.ts b/packages/propel/src/avatar/index.ts
index 522264e6..af722d71 100644
--- a/packages/propel/src/avatar/index.ts
+++ b/packages/propel/src/avatar/index.ts
@@ -5,3 +5,4 @@
*/
export * from "./avatar";
+export * from "./avatar-group";
diff --git a/packages/propel/src/spinners/circular-spinner.tsx b/packages/propel/src/spinners/circular-spinner.tsx
index e64f8c08..ae742696 100644
--- a/packages/propel/src/spinners/circular-spinner.tsx
+++ b/packages/propel/src/spinners/circular-spinner.tsx
@@ -6,7 +6,7 @@
import * as React from "react";
// helpers
-import clsx from "clsx";
+import { cn } from "../utils/classname";
export interface ISpinner extends React.SVGAttributes {
height?: string;
@@ -21,7 +21,7 @@ export function Spinner({ height = "32px", width = "32px", className = "" }: ISp
aria-hidden="true"
height={height}
width={width}
- className={clsx("animate-spin text-secondary", className)}
+ className={cn("animate-spin fill-accent-primary text-secondary", className)}
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
@@ -31,8 +31,8 @@ export function Spinner({ height = "32px", width = "32px", className = "" }: ISp
fill="currentColor"
/>
Loading...
diff --git a/packages/propel/src/tooltip/root.tsx b/packages/propel/src/tooltip/root.tsx
index d90bf71b..da3c675e 100644
--- a/packages/propel/src/tooltip/root.tsx
+++ b/packages/propel/src/tooltip/root.tsx
@@ -31,12 +31,13 @@ export function Tooltip(props: ITooltipProps) {
const {
tooltipHeading,
tooltipContent,
- position = "top",
+ position,
children,
disabled = false,
className = "",
openDelay = 200,
- side = "bottom",
+ // `position` wins when it is given, so it must not be defaulted: with `position = "top"` in this list the branch below always took it and `side`/`align` were declared props that could never reach the positioner. The defaults live here instead, and "top"/"center" is exactly what `convertPlacementToSideAndAlign("top")` returned, so every existing call site that passes neither is placed where it was.
+ side = "top",
align = "center",
sideOffset = 10,
closeDelay,
diff --git a/packages/ui/src/avatar/avatar.stories.tsx b/packages/ui/src/avatar/avatar.stories.tsx
deleted file mode 100644
index 9f96059f..00000000
--- a/packages/ui/src/avatar/avatar.stories.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-/**
- * Copyright (c) 2023-present Plane Software, Inc. and contributors
- * SPDX-License-Identifier: AGPL-3.0-only
- * See the LICENSE file for details.
- */
-
-import type { Meta, StoryObj } from "@storybook/react";
-import { Avatar } from "./avatar";
-
-const meta: Meta = {
- title: "Avatar",
- component: Avatar,
-};
-
-export default meta;
-type Story = StoryObj;
-
-export const Default: Story = {
- args: { name: "John Doe" },
-};
-
-export const Large: Story = {
- args: { name: "John Doe" },
-};
diff --git a/packages/ui/src/avatar/avatar.tsx b/packages/ui/src/avatar/avatar.tsx
deleted file mode 100644
index 1d29b047..00000000
--- a/packages/ui/src/avatar/avatar.tsx
+++ /dev/null
@@ -1,111 +0,0 @@
-/**
- * Copyright (c) 2023-present Plane Software, Inc. and contributors
- * SPDX-License-Identifier: AGPL-3.0-only
- * See the LICENSE file for details.
- */
-
-// ui
-import { Tooltip } from "@pace/propel/tooltip";
-// helpers
-import { cn } from "../utils";
-import type { TAvatarSize } from "./helper";
-import { getBorderRadius, getSizeInfo, isAValidNumber } from "./helper";
-
-type Props = {
- /**
- * The name of the avatar which will be displayed on the tooltip
- */
- name?: string;
- /**
- * The background color if the avatar image fails to load
- */
- fallbackBackgroundColor?: string;
- /**
- * The text to display if the avatar image fails to load
- */
- fallbackText?: string;
- /**
- * The text color if the avatar image fails to load
- */
- fallbackTextColor?: string;
- /**
- * Whether to show the tooltip or not
- * @default true
- */
- showTooltip?: boolean;
- /**
- * The size of the avatars
- * Possible values: "sm", "md", "base", "lg"
- * @default "md"
- */
- size?: TAvatarSize;
- /**
- * The shape of the avatar
- * Possible values: "circle", "square"
- * @default "circle"
- */
- shape?: "circle" | "square";
- /**
- * The source of the avatar image
- */
- src?: string;
- /**
- * The custom CSS class name to apply to the component
- */
- className?: string;
-};
-
-export function Avatar(props: Props) {
- const {
- name,
- fallbackBackgroundColor,
- fallbackText,
- fallbackTextColor,
- showTooltip = true,
- size = "md",
- shape = "circle",
- src,
- className = "",
- } = props;
-
- // get size details based on the size prop
- const sizeInfo = getSizeInfo(size);
-
- return (
-
-
- );
-}
diff --git a/packages/ui/src/spinners/index.ts b/packages/ui/src/spinners/index.ts
deleted file mode 100644
index 6f87ad95..00000000
--- a/packages/ui/src/spinners/index.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-/**
- * Copyright (c) 2023-present Plane Software, Inc. and contributors
- * SPDX-License-Identifier: AGPL-3.0-only
- * See the LICENSE file for details.
- */
-
-export * from "./circular-spinner";
From c9a6245d7c6c5663160be246d1aca0bbfbed0b40 Mon Sep 17 00:00:00 2001
From: Evan Hu
Date: Sat, 19 Sep 2026 21:45:20 +0900
Subject: [PATCH 5/5] refactor(web): take the last @pace/ui Loader onto
propel's Skeleton
The lazy-loading pass that landed on main after this branch was cut introduced
a fresh `Loader` Suspense fallback in the sidebar progress chart, and this
branch retires `Loader` from @pace/ui. Neither side conflicts textually, so the
rebase produced a tree that does not compile. Converted by the branch's own
`ui-loader-to-propel-skeleton` codemod, the same way every other call site was.
---
apps/web/core/components/core/sidebar/progress-chart.tsx | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/apps/web/core/components/core/sidebar/progress-chart.tsx b/apps/web/core/components/core/sidebar/progress-chart.tsx
index f743a746..d4ca935d 100644
--- a/apps/web/core/components/core/sidebar/progress-chart.tsx
+++ b/apps/web/core/components/core/sidebar/progress-chart.tsx
@@ -7,7 +7,7 @@
import React, { lazy, Suspense } from "react";
// pace imports
import type { TChartData, TModuleCompletionChartDistribution } from "@pace/types";
-import { Loader } from "@pace/ui";
+import { Skeleton } from "@pace/propel/skeleton";
import { renderFormattedDateWithoutYear } from "@pace/utils";
const AreaChart = lazy(function AreaChart() {
@@ -34,9 +34,9 @@ function ProgressChart({ distribution, totalIssues, className = "", plotTitle =