diff --git a/HDESIGN-CODE.md b/HDESIGN-CODE.md index ff9128a..4db421a 100644 --- a/HDESIGN-CODE.md +++ b/HDESIGN-CODE.md @@ -52,6 +52,7 @@ - `Принять` - `Добавить запрос` - любые акцентные toolbar-кнопки + - это правило обязательно и для `Внешних контуров`: `Добавить запрос` не может иметь светлый текст на светлом фоне - Save/update button: - если это зафиксированный green CTA, текст должен быть контрастным и читаемым - hover осветляет текущий тон, а не уходит в синий @@ -91,6 +92,9 @@ - assignee bubbles - дата должны быть симметричны верхней +- Для списков карточек `Внешних контуров` используется тот же вертикальный ритм, что и у `Внутреннего контура`: + - контейнер списка не плотнее `space-y-3` + - нельзя лепить карточки вплотную друг к другу ## Dropdown и popup - Все dropdown/popup приводятся к единому matte glass канону. @@ -118,6 +122,9 @@ - клиппинг - налезание на соседние блоки - старую “врезанную” верстку +- Для `Свойств` в `Внешних контурах` dropdown по умолчанию открывается вверх: + - `placement="top-start"` + - причина: блок находится близко к `Активности`, popup не должен падать вниз в соседнюю секцию ### Portal anchor snippet ```tsx @@ -222,12 +229,17 @@ - Для `Внешних контуров` это значит: - список карточек правится на уровне `list-item.tsx`, а не через внешний wrapper - gap между карточками должен совпадать с каноном `Внутреннего контура` - - актуальный gap списка на текущем каноне: `space-y-2` + - актуальный gap списка на текущем каноне: `space-y-3` + - при tab switch между `Открытые / Закрытые` нельзя полагаться только на route param; нужен локальный `pendingTab`, чтобы stale layout не мелькал до завершения refetch - toolbar-навигация и inline actions не должны использовать старые квадратные `IconButton` остатки - свойства `Приоритет / Метки / Статус` не должны рисовать внутренние boxed-chip артефакты + - popup `Приоритет / Метки` не может визуально жить внутри property-row; если он открывается из blur-shell, он обязан уходить в portal и рендериться над секцией - filled CTA вроде `Добавить запрос` используют `nodedc-external-primary-button` и всегда имеют тёмный текст - filled CTA используют чёрный/почти-чёрный текст всегда; белый текст на светлом акценте запрещён - secondary meta-иконки в карточке списка не должны иметь отдельную серую подложку, если по канону это простой inline icon + - empty-state не должен использовать декоративную серую подложку под SVG; media-box прозрачный, SVG выравнивается через `display:flex` и центрирование + - detail-toolbar в карточке запроса использует общий glass-cluster для листания `prev/next`, а сами кнопки внутри кластера — круглые, без квадратной подложки + - `Добавить запрос` в header `Внешних контуров` — это filled accent CTA с тёмным текстом, каноничным радиусом и hover в более светлый тон того же акцента - popup выбора `Приоритет / Метки` внутри detail view не рендерится inline в property-row; он обязан уходить в `portal` - секции с dropdown-trigger внутри blur/glass shell обязаны иметь `overflow: visible` и `isolation: isolate`, иначе popup визуально “тонет” внутри блока - при переключении `Открытые / Закрытые` store обязан очистить stale request list до нового fetch, чтобы пользователь не видел flash старой верстки @@ -244,13 +256,42 @@ - List spacing: ```tsx -
+
{filteredRequestIds.map((requestId) => ( ))}
``` +- Pending tab anti-flash: +```tsx +const [pendingTab, setPendingTab] = useState(null); +const routeTab = (searchParams.get("currentTab") as TInboxIssueCurrentTab | null) ?? currentTab; +const resolvedTab = pendingTab ?? routeTab; +const isTabTransitioning = loader === "init-loading" || pendingTab !== null || routeTab !== currentTab; +``` + +- Property popup anchor: +```tsx + + ... +
+ } +/> +``` + +- Detail toolbar cluster: +```tsx +
+ + +
+``` + - Property control: ```tsx
@@ -261,8 +302,15 @@ - Root tab switch without stale flash: ```tsx -void handleCurrentTab(workspaceSlug, projectId, nextTab); -router.push(`...currentTab=${nextTab}`); +const [pendingTab, setPendingTab] = useState(null); +const resolvedTab = pendingTab ?? routeTab; +const isTabTransitioning = loader === "init-loading" || pendingTab !== null || routeTab !== currentTab; + +if (resolvedTab !== nextTab) { + setPendingTab(nextTab); + void handleCurrentTab(workspaceSlug, projectId, nextTab); + router.push(`...currentTab=${nextTab}`); +} ``` - Store-side tab reset: @@ -281,6 +329,20 @@ const { styles, attributes } = usePopper(referenceElement, popperElement, { }); ``` +- Property popup without boxed artifact: +```tsx + + + ... +
+ } +/> +``` + - Контейнер секции с trigger: ```tsx
diff --git a/plane-src/apps/web/ce/components/projects/external-contours/issue-properties.tsx b/plane-src/apps/web/ce/components/projects/external-contours/issue-properties.tsx index 2cfa98c..0dba6b6 100644 --- a/plane-src/apps/web/ce/components/projects/external-contours/issue-properties.tsx +++ b/plane-src/apps/web/ce/components/projects/external-contours/issue-properties.tsx @@ -47,6 +47,7 @@ export const ExternalContoursIssueContentProperties = observer(function External value={issue?.priority} onChange={(val) => issue?.id && issueOperations.update(workspaceSlug, targetProjectId, issue.id, { priority: val })} disabled={!isEditable} + placement="top-start" buttonVariant="transparent-without-text" className="flex-1 overflow-visible" buttonContainerClassName="nodedc-external-property-control-shell h-full w-full overflow-visible rounded-[1.25rem] border-0 bg-transparent shadow-none outline-none" @@ -78,6 +79,7 @@ export const ExternalContoursIssueContentProperties = observer(function External onChange={(labelIds) => issue?.id && issueOperations.update(workspaceSlug, targetProjectId, issue.id, { label_ids: labelIds })} projectId={targetProjectId} disabled={!isEditable} + placement="top-start" rootClassName="w-full overflow-visible" buttonContainerClassName="nodedc-external-property-control-shell h-full w-full overflow-visible rounded-[1.25rem] border-0 bg-transparent shadow-none outline-none" label={ diff --git a/plane-src/apps/web/ce/components/projects/external-contours/list-item.tsx b/plane-src/apps/web/ce/components/projects/external-contours/list-item.tsx index 63034c0..82d697e 100644 --- a/plane-src/apps/web/ce/components/projects/external-contours/list-item.tsx +++ b/plane-src/apps/web/ce/components/projects/external-contours/list-item.tsx @@ -132,7 +132,7 @@ export const ExternalContoursListItem = observer(function ExternalContoursListIt {issue.priority && issue.priority !== "none" && ( -
+
diff --git a/plane-src/apps/web/ce/components/projects/external-contours/sidebar.tsx b/plane-src/apps/web/ce/components/projects/external-contours/sidebar.tsx index a4fc116..20c373a 100644 --- a/plane-src/apps/web/ce/components/projects/external-contours/sidebar.tsx +++ b/plane-src/apps/web/ce/components/projects/external-contours/sidebar.tsx @@ -4,7 +4,7 @@ * See the LICENSE file for details. */ -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { observer } from "mobx-react"; import { useTranslation } from "@plane/i18n"; import type { TInboxIssueCurrentTab } from "@plane/types"; @@ -35,16 +35,24 @@ export const ExternalContoursSidebar = observer(function ExternalContoursSidebar const { t } = useTranslation(); const { currentTab, filteredRequestIds, openRequestIds, closedRequestIds, loader, handleCurrentTab } = useProjectExternalContours(); + const [pendingTab, setPendingTab] = useState(null); const routeTab = (searchParams.get("currentTab") as TInboxIssueCurrentTab | null) ?? currentTab; - const isTabTransitioning = loader === "init-loading" || routeTab !== currentTab; + const resolvedTab = pendingTab ?? routeTab; + const isTabTransitioning = loader === "init-loading" || pendingTab !== null || routeTab !== currentTab; + + useEffect(() => { + if (pendingTab && loader !== "init-loading" && routeTab === pendingTab && currentTab === pendingTab) { + setPendingTab(null); + } + }, [currentTab, loader, pendingTab, routeTab]); useEffect(() => { if (workspaceSlug && projectId && filteredRequestIds.length > 0 && inboxIssueId === undefined) { router.push( - `/${workspaceSlug}/projects/${projectId}/external-contours?currentTab=${routeTab}&inboxIssueId=${filteredRequestIds[0]}` + `/${workspaceSlug}/projects/${projectId}/external-contours?currentTab=${resolvedTab}&inboxIssueId=${filteredRequestIds[0]}` ); } - }, [filteredRequestIds, inboxIssueId, projectId, routeTab, router, workspaceSlug]); + }, [filteredRequestIds, inboxIssueId, projectId, resolvedTab, router, workspaceSlug]); return (
@@ -57,12 +65,13 @@ export const ExternalContoursSidebar = observer(function ExternalContoursSidebar
) : filteredRequestIds.length > 0 ? ( -
+
{filteredRequestIds.map((requestId) => ( option.query.toLowerCase().includes(query.toLowerCase())); const { styles, attributes } = usePopper(referenceElement, popperElement, { + strategy: "fixed", placement: "bottom-start", modifiers: [ { @@ -129,90 +131,93 @@ export const IssueLabelSelect = observer(function IssueLabelSelect(props: IIssue - -
-
-
- - setQuery(e.target.value)} - placeholder={t("common.search.label")} - displayValue={(assigned: any) => assigned?.name} - onKeyDown={searchInputKeyDown} - tabIndex={baseTabIndex} - /> -
-
-
- {isLoading ? ( -

{t("common.loading")}

- ) : filteredOptions.length > 0 ? ( - filteredOptions.map((option) => ( - - `nodedc-dropdown-option cursor-pointer ${ - selected ? "text-primary" : "text-secondary" - }` - } - > - {({ selected }) => ( - <> - {option.content} - {selected && ( -
- -
+ {typeof document !== "undefined" && + createPortal( + +
+
+
+ + setQuery(e.target.value)} + placeholder={t("common.search.label")} + displayValue={(assigned: any) => assigned?.name} + onKeyDown={searchInputKeyDown} + tabIndex={baseTabIndex} + /> +
+
+
+ {isLoading ? ( +

{t("common.loading")}

+ ) : filteredOptions.length > 0 ? ( + filteredOptions.map((option) => ( + + `nodedc-dropdown-option cursor-pointer ${ + selected ? "text-primary" : "text-secondary" + }` + } + > + {({ selected }) => ( + <> + {option.content} + {selected && ( +
+ +
+ )} + )} - - )} -
- )) - ) : submitting ? ( - - ) : canCreateLabel ? ( - { - e.preventDefault(); - e.stopPropagation(); - if (!query.length) return; - handleAddLabel(query); - }} - className={`rounded-[0.9rem] px-2 py-2 text-left text-secondary ${query.length ? "cursor-pointer hover:bg-white/6" : "cursor-default"}`} - > - {query.length ? ( - <> - {/* TODO: Translate here */}+ Add "{query}" to - labels - + + )) + ) : submitting ? ( + + ) : canCreateLabel ? ( + { + e.preventDefault(); + e.stopPropagation(); + if (!query.length) return; + handleAddLabel(query); + }} + className={`rounded-[0.9rem] px-2 py-2 text-left text-secondary ${query.length ? "cursor-pointer hover:bg-white/6" : "cursor-default"}`} + > + {query.length ? ( + <> + + {t("label.create.type")} "{query}" + + ) : ( + t("label.create.type") + )} + ) : ( - t("label.create.type") +

+ {t("common.search.no_matching_results")} +

)} - - ) : ( -

- {t("common.search.no_matching_results")} -

- )} +
-
- + , + document.body + )} ); diff --git a/plane-src/apps/web/styles/globals.css b/plane-src/apps/web/styles/globals.css index 9a77d51..ac810c0 100644 --- a/plane-src/apps/web/styles/globals.css +++ b/plane-src/apps/web/styles/globals.css @@ -1162,15 +1162,17 @@ flex-direction: column; align-items: center; justify-content: center; - gap: 1rem; + gap: 1.1rem; text-align: center; } .nodedc-external-empty-media { - display: grid; - place-items: center; + display: flex; + align-items: center; + justify-content: center; width: 6.25rem; height: 6.25rem; + margin-inline: auto; border-radius: 0; background: transparent !important; box-shadow: none !important; @@ -1244,6 +1246,8 @@ gap: 0.6rem; position: relative; z-index: 1; + overflow: visible !important; + isolation: isolate; border: 0 !important; outline: none !important; box-shadow: none !important; @@ -1262,6 +1266,8 @@ gap: 0.6rem; position: relative; z-index: 1; + overflow: visible !important; + isolation: isolate; border: 0 !important; outline: none !important; box-shadow: none !important; @@ -1280,6 +1286,16 @@ padding: 0 !important; } + .nodedc-external-property-value > button, + .nodedc-external-property-value > button *, + .nodedc-external-property-control-shell, + .nodedc-external-property-control-shell * { + border: 0 !important; + outline: none !important; + box-shadow: none !important; + background: transparent !important; + } + .nodedc-external-property-control-shell:hover, .nodedc-external-property-control-shell:focus, .nodedc-external-property-control-shell:focus-visible, @@ -1310,15 +1326,21 @@ display: flex; align-items: center; gap: 0.5rem; - padding: 0; + padding: 0.25rem !important; border-radius: 999px; - background: transparent !important; + background: rgba(255, 255, 255, 0.05) !important; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.018) !important; } .nodedc-external-priority-inline { display: inline-flex; align-items: center; + justify-content: center; gap: 0.375rem; + width: auto !important; + min-width: 0 !important; + height: auto !important; + padding: 0 !important; color: var(--text-color-secondary) !important; background: transparent !important; box-shadow: none !important;