From b3d0c092a718f7af3a33b0005e5c04a9846d8bef Mon Sep 17 00:00:00 2001 From: Pedro Filho Date: Mon, 19 Oct 2026 11:56:34 -0300 Subject: [PATCH] refactor: route console warnings through one core helper and drop lint suppressions --- .changeset/shared-console-warnings.md | 6 +++ apps/docs/app/layout.tsx | 5 ++- apps/docs/components/search-dialog.tsx | 37 +++++++++++++++++++ packages/core/README.md | 3 ++ packages/core/src/index.ts | 1 + .../lib/warn-once.ts => core/src/lib/warn.ts} | 11 ++++-- packages/core/src/model/builders.ts | 7 ++-- packages/core/src/model/serialize.ts | 5 ++- packages/core/src/state/docking.ts | 6 +-- .../react/src/components/dashfoo-layout.tsx | 14 +++---- packages/react/src/components/floats.test.tsx | 2 + .../src/components/tabset/tabset-store.ts | 2 +- .../src/components/tabset/tabset-tab.tsx | 2 +- .../src/components/tabset/tabset-toolbar.tsx | 2 +- packages/react/src/drag/drag-hooks.ts | 3 +- packages/react/src/drag/drag-layer.tsx | 3 +- packages/react/src/drag/drag-subject.ts | 11 ++---- packages/react/src/hooks/persistence.ts | 28 +++++--------- packages/react/src/hooks/store.ts | 3 +- packages/react/src/hooks/use-float-gesture.ts | 3 +- scripts/launch/consumer-runtime.mjs.fixture | 2 - 21 files changed, 97 insertions(+), 59 deletions(-) create mode 100644 .changeset/shared-console-warnings.md create mode 100644 apps/docs/components/search-dialog.tsx rename packages/{react/src/lib/warn-once.ts => core/src/lib/warn.ts} (57%) diff --git a/.changeset/shared-console-warnings.md b/.changeset/shared-console-warnings.md new file mode 100644 index 00000000..da71a9d3 --- /dev/null +++ b/.changeset/shared-console-warnings.md @@ -0,0 +1,6 @@ +--- +"@dashfoo/core": minor +"@dashfoo/react": patch +--- + +Export `warn` and `warnOnce` from `@dashfoo/core`. Both packages now report degraded paths through them, so every developer-console warning carries the same `[dashfoo]` prefix, and a missing tab component warns once per component key across layouts. diff --git a/apps/docs/app/layout.tsx b/apps/docs/app/layout.tsx index 5905e3c2..9f95d91c 100644 --- a/apps/docs/app/layout.tsx +++ b/apps/docs/app/layout.tsx @@ -4,6 +4,8 @@ import { RootProvider } from "fumadocs-ui/provider/next"; import type { Metadata, Viewport } from "next"; import type { ReactNode } from "react"; +import { DocsSearchDialog } from "@/components/search-dialog"; + export const metadata: Metadata = { description: "Documentation for dashfoo: a headless React docking-layout library with tiled, resizable, tabbed regions, a serializable model, and zero imposed styling.", @@ -28,8 +30,7 @@ const RootLayout = ({ children }: { children: ReactNode }) => ( aria-hidden="true" className="reading-progress-bar reading-progress-range fixed top-0 left-0 z-50 h-0.5 w-0 bg-(--primary)" /> - {/* oxlint-disable-next-line typescript/no-deprecated -- fumadocs still supports the static preset; swapping to a hand-rolled Orama dialog is out of scope for the lint rollout. */} - {children} + {children} ); diff --git a/apps/docs/components/search-dialog.tsx b/apps/docs/components/search-dialog.tsx new file mode 100644 index 00000000..7d803778 --- /dev/null +++ b/apps/docs/components/search-dialog.tsx @@ -0,0 +1,37 @@ +"use client"; + +import { useDocsSearch } from "fumadocs-core/search/client"; +import { staticClient } from "fumadocs-core/search/client/orama-static"; +import type { SharedProps } from "fumadocs-ui/components/dialog/search"; +import { + SearchDialog, + SearchDialogClose, + SearchDialogContent, + SearchDialogHeader, + SearchDialogIcon, + SearchDialogInput, + SearchDialogList, + SearchDialogOverlay, +} from "fumadocs-ui/components/dialog/search"; +import type { ReactNode } from "react"; + +const client = staticClient(); + +const DocsSearchDialog = (props: SharedProps): ReactNode => { + const { query, search, setSearch } = useDocsSearch({ client }); + return ( + + + + + + + + + + + + ); +}; + +export { DocsSearchDialog }; diff --git a/packages/core/README.md b/packages/core/README.md index 08ed44ae..946092c7 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -427,6 +427,9 @@ types `SnapGrid`, `SnapDecision`. `machines`: `dragDockMachine`, `dropAction`; types `DragState`, `DragSubject`, `DropResolution`. +`warn`: `warn`, `warnOnce`. Both prefix the message with `[dashfoo]` and write it +with `console.warn`; `@dashfoo/react` reports its degraded paths through them. + ## License MIT diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 3bb4284c..f1c8662a 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -14,3 +14,4 @@ export * from "./state/reducer"; export * from "./model/schema"; export * from "./model/serialize"; export * from "./model/tree"; +export { warn, warnOnce } from "./lib/warn"; diff --git a/packages/react/src/lib/warn-once.ts b/packages/core/src/lib/warn.ts similarity index 57% rename from packages/react/src/lib/warn-once.ts rename to packages/core/src/lib/warn.ts index 7a64d05c..c04bcd22 100644 --- a/packages/react/src/lib/warn-once.ts +++ b/packages/core/src/lib/warn.ts @@ -1,12 +1,17 @@ const warned = new Set(); +const warn = (message: string, cause?: unknown): void => { + const text = `[dashfoo] ${message}`; + // oxlint-disable-next-line no-console -- dashfoo reports degraded paths on the developer console instead of failing silently + console.warn(...(cause === undefined ? [text] : [text, cause])); +}; + const warnOnce = (key: string, message: string): void => { if (warned.has(key)) { return; } warned.add(key); - // oxlint-disable-next-line no-console -- dashfoo reports degraded paths on the developer console instead of failing silently - console.warn(`[dashfoo] ${message}`); + warn(message); }; -export { warnOnce }; +export { warn, warnOnce }; diff --git a/packages/core/src/model/builders.ts b/packages/core/src/model/builders.ts index 2c74056a..fde8a3a1 100644 --- a/packages/core/src/model/builders.ts +++ b/packages/core/src/model/builders.ts @@ -1,3 +1,5 @@ +import { warn } from "../lib/warn"; + import { createNodeId } from "./ids"; import { clampSelected } from "./invariants"; import { DEFAULT_WEIGHT } from "./schema"; @@ -122,9 +124,8 @@ const model = (layout: RowNode, options: ModelOptions = {}): Dashfoo => { }; const duplicates = findDuplicateIds(built); if (duplicates.length > 0) { - // oxlint-disable-next-line no-console -- dashfoo reports degraded paths on the developer console instead of failing silently - console.warn( - `[dashfoo] builder produced duplicate node ids: ${duplicates.join(", ")}. Pass explicit ids when reusing a component`, + warn( + `builder produced duplicate node ids: ${duplicates.join(", ")}. Pass explicit ids when reusing a component`, ); } return built; diff --git a/packages/core/src/model/serialize.ts b/packages/core/src/model/serialize.ts index 8939dc95..27c39881 100644 --- a/packages/core/src/model/serialize.ts +++ b/packages/core/src/model/serialize.ts @@ -1,3 +1,5 @@ +import { warn } from "../lib/warn"; + import { normalize } from "./invariants"; import type { Dashfoo } from "./schema"; import { dashfooSchema, jsonValueSchema } from "./schema"; @@ -8,8 +10,7 @@ const parseModel = (value: unknown): Dashfoo => { const model = normalize(dashfooSchema.parse(value)); const duplicates = findDuplicateIds(model); if (duplicates.length > 0) { - // oxlint-disable-next-line no-console -- dashfoo reports degraded paths on the developer console instead of failing silently - console.warn(`[dashfoo] duplicate node ids in the loaded layout: ${duplicates.join(", ")}`); + warn(`duplicate node ids in the loaded layout: ${duplicates.join(", ")}`); } return model; }; diff --git a/packages/core/src/state/docking.ts b/packages/core/src/state/docking.ts index 4a7b9164..4babb340 100644 --- a/packages/core/src/state/docking.ts +++ b/packages/core/src/state/docking.ts @@ -1,4 +1,5 @@ import { assertNever } from "../lib/assert-never"; +import { warn } from "../lib/warn"; import { floatNode, row, tabset } from "../model/builders"; import type { Dashfoo, Geometry, RowNode, TabNode, TabsetNode } from "../model/schema"; import { @@ -145,9 +146,8 @@ const addTab = ( { index, location, tab, targetId }: Extract, ): TabsetNode | undefined => { if (isIdInUse(draft, tab.id)) { - // oxlint-disable-next-line no-console -- dashfoo reports degraded paths on the developer console instead of failing silently - console.warn( - `[dashfoo] addNode ignored: node id "${tab.id}" is already in the layout. Pass a fresh id, e.g. createTabId()`, + warn( + `addNode ignored: node id "${tab.id}" is already in the layout. Pass a fresh id, e.g. createTabId()`, ); return undefined; } diff --git a/packages/react/src/components/dashfoo-layout.tsx b/packages/react/src/components/dashfoo-layout.tsx index 5e87716a..9f249ca6 100644 --- a/packages/react/src/components/dashfoo-layout.tsx +++ b/packages/react/src/components/dashfoo-layout.tsx @@ -1,7 +1,7 @@ "use client"; import type { Action, Dashfoo, DockLocation, SnapConfig, TabNode, TabsetNode } from "@dashfoo/core"; -import { findTabset } from "@dashfoo/core"; +import { findTabset, warnOnce } from "@dashfoo/core"; import type { ComponentType, ReactNode } from "react"; import { forwardRef, useCallback, useImperativeHandle, useMemo } from "react"; @@ -14,8 +14,6 @@ import { Layout } from "./layout"; type TabComponent = ComponentType<{ node: TabNode }>; -const warnedComponents = new Set(); - type DashfooHandle = { addTab: ( tab: TabNode, @@ -155,11 +153,11 @@ const DashfooLayout = forwardRef((props, ref) } const Component = components?.[tab.component]; if (!Component) { - if (components && !warnedComponents.has(tab.component)) { - warnedComponents.add(tab.component); - - // oxlint-disable-next-line no-console -- dashfoo reports degraded paths on the developer console instead of failing silently - console.warn(`[dashfoo] no component registered for "${tab.component}"`); + if (components) { + warnOnce( + `unregistered-component:${tab.component}`, + `no component registered for "${tab.component}"`, + ); } return null; } diff --git a/packages/react/src/components/floats.test.tsx b/packages/react/src/components/floats.test.tsx index c6f30c2f..3bff76e6 100644 --- a/packages/react/src/components/floats.test.tsx +++ b/packages/react/src/components/floats.test.tsx @@ -94,6 +94,7 @@ describe("floating panels", () => { panel.setPointerCapture = () => {}; panel.releasePointerCapture = () => {}; + panel.hasPointerCapture = () => true; fireEvent.pointerDown(titleBar, { buttons: 1, clientX: 0, clientY: 0, pointerId: 1 }); fireEvent.pointerMove(panel, { buttons: 1, clientX: 40, clientY: 30, pointerId: 1 }); @@ -240,6 +241,7 @@ describe("floating panels", () => { panel.setPointerCapture = () => {}; panel.releasePointerCapture = () => {}; + panel.hasPointerCapture = () => true; const titleBar = panel.querySelector('[data-dashfoo="float-titlebar"]')!; const startLeft = panel.style.getPropertyValue("--dashfoo-float-left"); const startTop = panel.style.getPropertyValue("--dashfoo-float-top"); diff --git a/packages/react/src/components/tabset/tabset-store.ts b/packages/react/src/components/tabset/tabset-store.ts index cb685b9a..c9fa257b 100644 --- a/packages/react/src/components/tabset/tabset-store.ts +++ b/packages/react/src/components/tabset/tabset-store.ts @@ -1,12 +1,12 @@ "use client"; import type { Action, TabNode, TabsetNode } from "@dashfoo/core"; +import { warnOnce } from "@dashfoo/core"; import { createContext, useContext } from "react"; import type { StoreApi } from "zustand"; import { createStore, useStore } from "zustand"; import { tabDomId } from "../../lib/tab-ids"; -import { warnOnce } from "../../lib/warn-once"; import type { OverflowItem } from "../tab-overflow-menu"; type TabsetState = { diff --git a/packages/react/src/components/tabset/tabset-tab.tsx b/packages/react/src/components/tabset/tabset-tab.tsx index c5937ec0..b64147fc 100644 --- a/packages/react/src/components/tabset/tabset-tab.tsx +++ b/packages/react/src/components/tabset/tabset-tab.tsx @@ -1,6 +1,7 @@ "use client"; import type { TabNode } from "@dashfoo/core"; +import { warnOnce } from "@dashfoo/core"; import type { ComponentProps, KeyboardEvent, MouseEvent, ReactNode } from "react"; import { forwardRef, useContext, useEffect, useMemo, useRef } from "react"; @@ -8,7 +9,6 @@ import { useDragSubject, useTabDraggable } from "../../drag/drag-hooks"; import { useLayout } from "../../hooks/layout-store"; import { mergeRefs } from "../../lib/merge-refs"; import { panelDomId, tabDomId } from "../../lib/tab-ids"; -import { warnOnce } from "../../lib/warn-once"; import { TabContext, TabsetStoreContext, useTab, useTabset } from "./tabset-store"; diff --git a/packages/react/src/components/tabset/tabset-toolbar.tsx b/packages/react/src/components/tabset/tabset-toolbar.tsx index b6d8e13f..d51709c9 100644 --- a/packages/react/src/components/tabset/tabset-toolbar.tsx +++ b/packages/react/src/components/tabset/tabset-toolbar.tsx @@ -1,5 +1,6 @@ "use client"; +import { warnOnce } from "@dashfoo/core"; import type { ComponentProps, MouseEvent, ReactNode } from "react"; import { forwardRef, useMemo } from "react"; @@ -7,7 +8,6 @@ import { useTabsetDraggable } from "../../drag/drag-hooks"; import { useLayout } from "../../hooks/layout-store"; import { measureFloatRect } from "../../lib/float-geometry"; import { mergeRefs } from "../../lib/merge-refs"; -import { warnOnce } from "../../lib/warn-once"; import { FloatIcon, GripIcon, MaximizeIcon } from "../close-icon"; import { useHasFloatLayer } from "../float-context"; diff --git a/packages/react/src/drag/drag-hooks.ts b/packages/react/src/drag/drag-hooks.ts index d86979d7..a99527e1 100644 --- a/packages/react/src/drag/drag-hooks.ts +++ b/packages/react/src/drag/drag-hooks.ts @@ -1,12 +1,11 @@ "use client"; import type { DragSubject, DropIntent, TabNode } from "@dashfoo/core"; +import { warnOnce } from "@dashfoo/core"; import { Draggable, Droppable } from "@dnd-kit/dom"; import { useSelector } from "@xstate/react"; import { useCallback, useContext, useEffect, useId, useMemo, useRef } from "react"; -import { warnOnce } from "../lib/warn-once"; - import { DragContext } from "./drag-context"; import type { DashfooDragManager } from "./drag-manager"; import type { DashfooDragData } from "./drag-subject"; diff --git a/packages/react/src/drag/drag-layer.tsx b/packages/react/src/drag/drag-layer.tsx index d334013a..7b9db0c8 100644 --- a/packages/react/src/drag/drag-layer.tsx +++ b/packages/react/src/drag/drag-layer.tsx @@ -1,12 +1,11 @@ "use client"; -import { resolveDropIntent } from "@dashfoo/core"; +import { resolveDropIntent, warnOnce } from "@dashfoo/core"; import type { ReactNode } from "react"; import { useContext, useEffect, useId, useMemo } from "react"; import type { LayoutStore } from "../hooks/layout-store"; import { LayoutStoreContext } from "../hooks/layout-store"; -import { warnOnce } from "../lib/warn-once"; import { DockIndicator } from "./dock-indicator"; import type { DragContextValue, RegisteredLayer } from "./drag-context"; diff --git a/packages/react/src/drag/drag-subject.ts b/packages/react/src/drag/drag-subject.ts index fad9acdf..1d6428fa 100644 --- a/packages/react/src/drag/drag-subject.ts +++ b/packages/react/src/drag/drag-subject.ts @@ -1,5 +1,5 @@ import type { DragSubject, TabNode } from "@dashfoo/core"; -import { tabNodeSchema } from "@dashfoo/core"; +import { tabNodeSchema, warn } from "@dashfoo/core"; import type { Data } from "@dnd-kit/abstract"; import type { Draggable } from "@dnd-kit/dom"; @@ -20,22 +20,19 @@ const subjectFor = (source: DragSource): DragSubject | null => { } if (data.type === "external") { if (!isTabFactory(data.createTab)) { - // oxlint-disable-next-line no-console -- dashfoo reports degraded paths on the developer console instead of failing silently - console.warn("[dashfoo] external drag source is missing its createTab function"); + warn("external drag source is missing its createTab function"); return null; } let candidate: ReturnType; try { candidate = data.createTab(); } catch (error) { - // oxlint-disable-next-line no-console -- dashfoo reports degraded paths on the developer console instead of failing silently - console.warn("[dashfoo] external drag source createTab threw", error); + warn("external drag source createTab threw", error); return null; } const parsed = tabNodeSchema.safeParse(candidate); if (!parsed.success) { - // oxlint-disable-next-line no-console -- dashfoo reports degraded paths on the developer console instead of failing silently - console.warn("[dashfoo] external drag source returned an invalid tab", parsed.error); + warn("external drag source returned an invalid tab", parsed.error); return null; } return { id: String(source.id), kind: "external", tab: parsed.data }; diff --git a/packages/react/src/hooks/persistence.ts b/packages/react/src/hooks/persistence.ts index 1f6f1caf..fb57e6b9 100644 --- a/packages/react/src/hooks/persistence.ts +++ b/packages/react/src/hooks/persistence.ts @@ -1,7 +1,7 @@ "use client"; import type { Dashfoo } from "@dashfoo/core"; -import { fromJSON, toJSON } from "@dashfoo/core"; +import { fromJSON, toJSON, warn } from "@dashfoo/core"; import { useCallback, useEffect, useMemo, useRef } from "react"; type StorageAdapter = { @@ -31,8 +31,7 @@ const localStorageAdapter: StorageAdapter = { try { return window.localStorage.getItem(key); } catch (error) { - // oxlint-disable-next-line no-console -- dashfoo reports degraded paths on the developer console instead of failing silently - console.warn("[dashfoo] failed to load persisted layout", error); + warn("failed to load persisted layout", error); return null; } }, @@ -43,8 +42,7 @@ const localStorageAdapter: StorageAdapter = { try { window.localStorage.removeItem(key); } catch (error) { - // oxlint-disable-next-line no-console -- dashfoo reports degraded paths on the developer console instead of failing silently - console.warn("[dashfoo] failed to clear persisted layout", error); + warn("failed to clear persisted layout", error); } }, setItem: (key, value) => { @@ -54,8 +52,7 @@ const localStorageAdapter: StorageAdapter = { try { window.localStorage.setItem(key, value); } catch (error) { - // oxlint-disable-next-line no-console -- dashfoo reports degraded paths on the developer console instead of failing silently - console.warn("[dashfoo] failed to persist layout", error); + warn("failed to persist layout", error); } }, }; @@ -89,8 +86,7 @@ const removeStoredLayout = ({ key, storage }: PersistTarget): void => { try { storage.removeItem(key); } catch (error) { - // oxlint-disable-next-line no-console -- dashfoo reports degraded paths on the developer console instead of failing silently - console.warn("[dashfoo] failed to clear persisted layout", error); + warn("failed to clear persisted layout", error); } }; @@ -103,14 +99,12 @@ const loadStoredLayout = (target: PersistTarget): Dashfoo | undefined => { try { return fromJSON(raw); } catch (error) { - // oxlint-disable-next-line no-console -- dashfoo reports degraded paths on the developer console instead of failing silently - console.warn("[dashfoo] discarding unreadable persisted layout", error); + warn("discarding unreadable persisted layout", error); removeStoredLayout(target); return undefined; } } catch (error) { - // oxlint-disable-next-line no-console -- dashfoo reports degraded paths on the developer console instead of failing silently - console.warn("[dashfoo] failed to load persisted layout", error); + warn("failed to load persisted layout", error); return undefined; } }; @@ -152,9 +146,8 @@ const useLayoutStorage = ( const loadedKey = useRef(key); useEffect(() => { if (key !== undefined && key !== loadedKey.current) { - // oxlint-disable-next-line no-console -- dashfoo reports degraded paths on the developer console instead of failing silently - console.warn( - "[dashfoo] persist key changed for a mounted layout; the displayed layout still reflects the previous key; remount (key={persistKey}) to load the new key", + warn( + "persist key changed for a mounted layout; the displayed layout still reflects the previous key; remount (key={persistKey}) to load the new key", ); loadedKey.current = key; } @@ -175,8 +168,7 @@ const useLayoutStorage = ( try { write.storage.setItem(write.key, write.value); } catch (error) { - // oxlint-disable-next-line no-console -- dashfoo reports degraded paths on the developer console instead of failing silently - console.warn("[dashfoo] failed to persist layout", error); + warn("failed to persist layout", error); } } }, []); diff --git a/packages/react/src/hooks/store.ts b/packages/react/src/hooks/store.ts index 5bc056cd..1e7f3bb5 100644 --- a/packages/react/src/hooks/store.ts +++ b/packages/react/src/hooks/store.ts @@ -10,12 +10,11 @@ import { redoHistory, reducer, undoHistory, + warnOnce, } from "@dashfoo/core"; import { useCallback, useMemo, useState } from "react"; import { createStore, useStore } from "zustand"; -import { warnOnce } from "../lib/warn-once"; - import type { PersistInput } from "./persistence"; import { useLayoutStorage } from "./persistence"; diff --git a/packages/react/src/hooks/use-float-gesture.ts b/packages/react/src/hooks/use-float-gesture.ts index d85cf38c..dee77455 100644 --- a/packages/react/src/hooks/use-float-gesture.ts +++ b/packages/react/src/hooks/use-float-gesture.ts @@ -12,8 +12,7 @@ const CHIP_SIZE: Size = { height: 34, width: 168 }; const TAP_SLOP = 4; const releaseCapture = (panel: HTMLElement, pointerId: number): void => { - // oxlint-disable-next-line typescript/no-unnecessary-condition -- jsdom and older engines lack hasPointerCapture despite the DOM typings - if (panel.hasPointerCapture?.(pointerId)) { + if (panel.hasPointerCapture(pointerId)) { panel.releasePointerCapture(pointerId); } }; diff --git a/scripts/launch/consumer-runtime.mjs.fixture b/scripts/launch/consumer-runtime.mjs.fixture index 8928d264..7054a3a3 100644 --- a/scripts/launch/consumer-runtime.mjs.fixture +++ b/scripts/launch/consumer-runtime.mjs.fixture @@ -1,5 +1,3 @@ -// oxlint-disable unicorn/max-nested-calls -- This standalone smoke fixture uses createElement to avoid requiring a JSX transpiler in consumers. -// oxlint-disable require-await, typescript/require-await -- Async act flushes hydration and passive effects in both supported React versions. import assert from "node:assert/strict"; import { model, row, tab, tabset } from "@dashfoo/core";