@noya-app/noya-designsystem 0.1.66 → 0.1.68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/.turbo/turbo-build.log +11 -11
  2. package/CHANGELOG.md +14 -0
  3. package/dist/index.css +1 -1
  4. package/dist/index.d.mts +81 -5
  5. package/dist/index.d.ts +81 -5
  6. package/dist/index.js +1205 -857
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +1047 -698
  9. package/dist/index.mjs.map +1 -1
  10. package/package.json +4 -2
  11. package/src/components/Anchor.tsx +30 -0
  12. package/src/components/BaseToolbar.tsx +11 -8
  13. package/src/components/Chip.tsx +16 -2
  14. package/src/components/Collection.tsx +10 -0
  15. package/src/components/Combobox.tsx +30 -21
  16. package/src/components/ComboboxMenu.tsx +49 -18
  17. package/src/components/Dialog.tsx +7 -1
  18. package/src/components/EditableText.tsx +7 -3
  19. package/src/components/Grid.tsx +2 -0
  20. package/src/components/GridView.tsx +52 -9
  21. package/src/components/List.tsx +54 -9
  22. package/src/components/ListView.tsx +44 -0
  23. package/src/components/ScrollArea2.tsx +56 -0
  24. package/src/components/SearchCompletionMenu.tsx +6 -2
  25. package/src/components/TreeView.tsx +4 -0
  26. package/src/components/UserPointer.tsx +34 -7
  27. package/src/components/connected-users-menu/ConnectedUsersMenuLayout.tsx +1 -3
  28. package/src/components/listView/ListViewEditableRowTitle.tsx +23 -1
  29. package/src/components/listView/ListViewRoot.tsx +45 -13
  30. package/src/components/sorting/Sortable.tsx +22 -10
  31. package/src/components/workspace/WorkspaceLayout.tsx +62 -4
  32. package/src/components/workspace/WorkspaceSideContext.tsx +18 -0
  33. package/src/components/workspace/renderPanelChildren.tsx +2 -2
  34. package/src/components/workspace/types.ts +8 -1
  35. package/src/contexts/DialogContext.tsx +22 -10
  36. package/src/index.css +143 -1
  37. package/src/index.tsx +4 -0
  38. package/src/theme/proseTheme.ts +2 -0
  39. package/src/utils/combobox.ts +17 -2
  40. package/src/utils/detailLineClamp.ts +33 -0
  41. package/tailwind.config.ts +3 -1
@@ -7,7 +7,13 @@ import {
7
7
  useSize,
8
8
  useWindowSize,
9
9
  } from "@noya-app/react-utils";
10
- import React, { memo, useImperativeHandle, useMemo, useRef } from "react";
10
+ import React, {
11
+ memo,
12
+ useCallback,
13
+ useImperativeHandle,
14
+ useMemo,
15
+ useRef,
16
+ } from "react";
11
17
  import { cx } from "../../utils/classNames";
12
18
  import { MenuItem } from "../internal/Menu";
13
19
  import { DrawerWorkspaceLayout } from "./DrawerWorkspaceLayout";
@@ -19,6 +25,7 @@ import {
19
25
  RenderPanel,
20
26
  SidebarRef,
21
27
  } from "./types";
28
+ import { WorkspaceSideProvider } from "./WorkspaceSideContext";
22
29
 
23
30
  export type SideType = "auto" | "panel" | "drawer";
24
31
 
@@ -280,13 +287,57 @@ export const WorkspaceLayout = forwardRefGeneric(function WorkspaceLayout<
280
287
  (left ? leftSidebarPercentage : 0) -
281
288
  (right ? rightSidebarPercentage : 0);
282
289
 
290
+ const isDrawerActive =
291
+ sideType === "drawer" ||
292
+ (sideType === "auto" && parentSize.width <= sideTypeBreakpoint);
293
+
294
+ const closeLeftSidebar = useCallback(() => {
295
+ if (isDrawerActive) {
296
+ leftSidebarRef.current?.collapse();
297
+ }
298
+ }, [isDrawerActive]);
299
+
300
+ const closeRightSidebar = useCallback(() => {
301
+ if (isDrawerActive) {
302
+ rightSidebarRef.current?.collapse();
303
+ }
304
+ }, [isDrawerActive]);
305
+
306
+ const leftChildrenProps = useMemo(() => {
307
+ return {
308
+ activeTabValue: leftTabValue,
309
+ closeSidebar: closeLeftSidebar,
310
+ };
311
+ }, [leftTabValue, closeLeftSidebar]);
312
+
313
+ const rightChildrenProps = useMemo(() => {
314
+ return {
315
+ activeTabValue: rightTabValue,
316
+ closeSidebar: closeRightSidebar,
317
+ };
318
+ }, [rightTabValue, closeRightSidebar]);
319
+
320
+ const leftSidebarProviderValue = useMemo(() => {
321
+ return {
322
+ closeSidebar: closeLeftSidebar,
323
+ };
324
+ }, [closeLeftSidebar]);
325
+
326
+ const rightSidebarProviderValue = useMemo(() => {
327
+ return {
328
+ closeSidebar: closeRightSidebar,
329
+ };
330
+ }, [closeRightSidebar]);
331
+
283
332
  const leftPanel =
284
333
  left && leftVisible !== false ? (
285
334
  <PanelInner
286
335
  style={leftStyle}
287
336
  className={cx("n-bg-sidebar-background n-flex-col", leftClassName)}
288
337
  >
289
- {leftTabValue ? renderPanelChildren(left, leftTabValue) : null}
338
+ <WorkspaceSideProvider value={leftSidebarProviderValue}>
339
+ {leftTabValue ? renderPanelChildren(left, leftChildrenProps) : null}
340
+ </WorkspaceSideProvider>
290
341
  </PanelInner>
291
342
  ) : null;
292
343
 
@@ -296,7 +347,11 @@ export const WorkspaceLayout = forwardRefGeneric(function WorkspaceLayout<
296
347
  style={rightStyle}
297
348
  className={cx("n-bg-sidebar-background n-flex-col", rightClassName)}
298
349
  >
299
- {rightTabValue ? renderPanelChildren(right, rightTabValue) : null}
350
+ <WorkspaceSideProvider value={rightSidebarProviderValue}>
351
+ {rightTabValue
352
+ ? renderPanelChildren(right, rightChildrenProps)
353
+ : null}
354
+ </WorkspaceSideProvider>
300
355
  </PanelInner>
301
356
  ) : null;
302
357
 
@@ -365,7 +420,10 @@ export const WorkspaceLayout = forwardRefGeneric(function WorkspaceLayout<
365
420
  <div
366
421
  ref={containerRef}
367
422
  id={id}
368
- className={cx("n-flex n-flex-col n-bg-canvas-background n-relative", className)}
423
+ className={cx(
424
+ "n-flex n-flex-col n-bg-canvas-background n-relative",
425
+ className
426
+ )}
369
427
  style={style}
370
428
  data-theme={theme}
371
429
  >
@@ -0,0 +1,18 @@
1
+ "use client";
2
+
3
+ import { createContext, useContext } from "react";
4
+
5
+ export type WorkspaceSideContextValue = {
6
+ /**
7
+ * Collapses the current sidebar when available (e.g., in drawer/mobile layout).
8
+ */
9
+ closeSidebar?: () => void;
10
+ };
11
+
12
+ const WorkspaceSideContext = createContext<WorkspaceSideContextValue>({});
13
+
14
+ export const WorkspaceSideProvider = WorkspaceSideContext.Provider;
15
+
16
+ export function useWorkspaceSide(): WorkspaceSideContextValue {
17
+ return useContext(WorkspaceSideContext);
18
+ }
@@ -2,10 +2,10 @@ import { RenderPanel } from "./types";
2
2
 
3
3
  export function renderPanelChildren<TabValue extends string>(
4
4
  fn: RenderPanel<TabValue>,
5
- activeTabValue: TabValue
5
+ props: { activeTabValue: TabValue; closeSidebar?: () => void }
6
6
  ) {
7
7
  if (typeof fn === "function") {
8
- return fn({ activeTabValue });
8
+ return fn(props);
9
9
  }
10
10
 
11
11
  return fn;
@@ -23,7 +23,14 @@ export type PanelLayoutState = {
23
23
 
24
24
  export type RenderPanel<TabValue extends string> =
25
25
  | ReactNode
26
- | ((props: { activeTabValue: TabValue }) => ReactNode);
26
+ | ((props: {
27
+ activeTabValue: TabValue;
28
+ /**
29
+ * When the layout is using a drawer (mobile), this will collapse
30
+ * the current sidebar. It may be undefined for panel layouts.
31
+ */
32
+ closeSidebar?: () => void;
33
+ }) => ReactNode);
27
34
 
28
35
  export type LayoutProps<
29
36
  LeftTab extends string = string,
@@ -65,7 +65,7 @@ const DialogContext = createContext<DialogContextValue | undefined>(undefined);
65
65
  type BaseDialogContents = {
66
66
  style?: React.CSSProperties;
67
67
  className?: string;
68
- title: string;
68
+ title: ReactNode;
69
69
  description?: ReactNode;
70
70
  };
71
71
 
@@ -228,6 +228,7 @@ export const DialogProvider = function DialogProvider({
228
228
  );
229
229
 
230
230
  const inputRef = useRef<HTMLInputElement>(null);
231
+ const confirmButtonRef = useRef<HTMLButtonElement>(null);
231
232
  const dialogRef = useRef<IDialog>(null);
232
233
 
233
234
  const containsElement = useCallback((element: HTMLElement) => {
@@ -268,16 +269,25 @@ export const DialogProvider = function DialogProvider({
268
269
  )}
269
270
  onOpenAutoFocus={useCallback(
270
271
  (event: Event) => {
271
- event.stopPropagation();
272
- event.preventDefault();
273
-
274
- if (!contents || contents.type !== "input") return;
272
+ if (!contents) return;
273
+
274
+ if (contents.type === "input") {
275
+ event.stopPropagation();
276
+ event.preventDefault();
277
+ inputRef.current?.focus();
278
+ inputRef.current?.setSelectionRange(
279
+ 0,
280
+ inputRef.current.value.length
281
+ );
282
+ return;
283
+ }
275
284
 
276
- inputRef.current?.focus();
277
- inputRef.current?.setSelectionRange(
278
- 0,
279
- inputRef.current.value.length
280
- );
285
+ if (contents.type === "confirmation") {
286
+ event.stopPropagation();
287
+ event.preventDefault();
288
+ confirmButtonRef.current?.focus();
289
+ return;
290
+ }
281
291
  },
282
292
  [contents]
283
293
  )}
@@ -295,6 +305,8 @@ export const DialogProvider = function DialogProvider({
295
305
  </Button>
296
306
  <Spacer.Horizontal size={16} />
297
307
  <Button
308
+ colorScheme="primary"
309
+ ref={confirmButtonRef}
298
310
  onClick={() => {
299
311
  contents.resolve(true);
300
312
  setContents(undefined);
package/src/index.css CHANGED
@@ -128,7 +128,28 @@
128
128
  --n-chip-primary-shadow: rgb(238, 229, 255);
129
129
  --n-chip-secondary-shadow: rgb(205, 238, 231);
130
130
  --n-chip-error-shadow: rgb(255, 219, 219);
131
- --n-chip-default-shadow: rgb(0, 0, 0);
131
+ --n-chip-default-shadow: rgba(0, 0, 0, 0.1);
132
+ /* Hover shadows for outlined Chip */
133
+ --n-chip-primary-shadow-hover: color-mix(
134
+ in srgb,
135
+ var(--n-chip-primary-shadow) 85%,
136
+ var(--n-text) 15%
137
+ );
138
+ --n-chip-secondary-shadow-hover: color-mix(
139
+ in srgb,
140
+ var(--n-chip-secondary-shadow) 85%,
141
+ var(--n-text) 15%
142
+ );
143
+ --n-chip-error-shadow-hover: color-mix(
144
+ in srgb,
145
+ var(--n-chip-error-shadow) 85%,
146
+ var(--n-text) 15%
147
+ );
148
+ --n-chip-default-shadow-hover: color-mix(
149
+ in srgb,
150
+ var(--n-chip-default-shadow) 80%,
151
+ var(--n-text) 20%
152
+ );
132
153
  --n-floating-button: rgb(248, 248, 250);
133
154
  --n-block-border: rgb(184, 201, 218);
134
155
  --n-block-highlight: rgb(255, 165, 0);
@@ -249,3 +270,124 @@
249
270
  color: var(--n-text-disabled);
250
271
  pointer-events: none;
251
272
  }
273
+
274
+ /* Direct child pre blocks */
275
+ .n-prose pre {
276
+ background-color: var(--n-indigo-25);
277
+ border-width: 1px;
278
+ border-style: solid;
279
+ border-color: var(--n-divider);
280
+ border-radius: 0.375rem; /* rounded-md */
281
+ }
282
+
283
+ /* Code inside pre */
284
+ .n-prose pre > code {
285
+ font-size: 12.8px;
286
+ }
287
+
288
+ /* Inline code (not inside pre): hide before/after content */
289
+ .n-prose code:not(:where(pre *))::before,
290
+ .n-prose code:not(:where(pre *))::after {
291
+ content: none !important;
292
+ }
293
+
294
+ /* Blockquote paragraphs: remove decorative quotes, reset style/weight */
295
+ .n-prose blockquote p::before,
296
+ .n-prose blockquote p::after {
297
+ content: none !important;
298
+ }
299
+
300
+ /* Inline code (not inside pre): background, border, padding, radius, weight */
301
+ .n-prose code:not(:where(pre *)) {
302
+ background-color: var(--n-indigo-25);
303
+ border-width: 1px;
304
+ border-style: solid;
305
+ border-color: var(--n-divider);
306
+ padding-left: 0.25rem; /* px-1 */
307
+ padding-right: 0.25rem; /* px-1 */
308
+ padding-top: 0.125rem; /* py-0.5 */
309
+ padding-bottom: 0.125rem; /* py-0.5 */
310
+ border-radius: 0.25rem; /* rounded */
311
+ font-weight: 400; /* font-normal */
312
+ }
313
+
314
+ .n-prose blockquote p {
315
+ font-weight: 400; /* font-normal */
316
+ font-style: normal; /* not-italic */
317
+ }
318
+
319
+ /* Task list container adjustments */
320
+ .n-prose .contains-task-list {
321
+ padding-left: 0; /* pl-0 */
322
+ margin-left: 0; /* ml-0 */
323
+ list-style-position: inside; /* list-inside */
324
+ }
325
+
326
+ /* Task list items: layout + spacing */
327
+ .n-prose .task-list-item {
328
+ list-style-type: none; /* list-none */
329
+ margin-left: 0; /* ml-0 */
330
+ padding-left: 1px; /* pl-px */
331
+ display: grid; /* grid */
332
+ grid-template-columns: auto 1fr; /* grid-cols-[auto,1fr] */
333
+ align-items: start; /* items-start */
334
+ column-gap: 0.5rem; /* gap-x-2 */
335
+ }
336
+
337
+ /* Checkbox column placement + visual tweaks */
338
+ .n-prose .task-list-item > .task-list-item-checkbox {
339
+ grid-column: 1 / 2; /* col-start-1 col-end-2 */
340
+ margin-top: 2px; /* mt-[2px] */
341
+ margin-right: 0; /* mr-0 */
342
+ accent-color: var(--n-primary); /* accent-[var(--n-primary)] */
343
+ }
344
+
345
+ /* Non-checkbox content spans the second column */
346
+ .n-prose .task-list-item > :not(.task-list-item-checkbox) {
347
+ grid-column: 2 / 3; /* col-start-2 col-end-3 */
348
+ }
349
+
350
+ /* Inline paragraphs/spans inside task items */
351
+ .n-prose .task-list-item > p,
352
+ .n-prose .task-list-item > span {
353
+ margin: 0; /* m-0 */
354
+ display: inline; /* inline */
355
+ }
356
+
357
+ /* Nested lists inside task items */
358
+ .n-prose .task-list-item > ul,
359
+ .n-prose .task-list-item > ol {
360
+ margin: 0; /* m-0 */
361
+ list-style-position: inside; /* list-inside */
362
+ }
363
+
364
+ /* When a list item is marked as a task item but has no checkbox,
365
+ collapse to a single-column grid so all children stack vertically. */
366
+ .n-prose .task-list-item:not(:has(> .task-list-item-checkbox)) {
367
+ grid-template-columns: 1fr;
368
+ }
369
+
370
+ .n-prose .task-list-item:not(:has(> .task-list-item-checkbox)) > * {
371
+ grid-column: 1 / -1;
372
+ }
373
+
374
+ /* Non-task list items inside a list that contains task items
375
+ should keep their native bullets; ensure nested lists break
376
+ onto the next line similar to GitHub's markdown rendering. */
377
+ .n-prose .contains-task-list > li:not(.task-list-item) > ul,
378
+ .n-prose .contains-task-list > li:not(.task-list-item) > ol {
379
+ display: block;
380
+ }
381
+
382
+ /* Ensure the first paragraph sits inline with the bullet */
383
+ .n-prose .contains-task-list > li:not(.task-list-item) > p {
384
+ display: inline;
385
+ margin: 0;
386
+ }
387
+
388
+ /* Spacing for nested unordered lists within mixed task/non-task lists */
389
+ .n-prose .contains-task-list > li:not(.task-list-item) > ul {
390
+ margin-left: 16px;
391
+ margin-top: 4px;
392
+ margin-bottom: 4px;
393
+ }
package/src/index.tsx CHANGED
@@ -3,6 +3,7 @@ export * from "./components/ActionMenu";
3
3
  export * from "./components/ActivityIndicator";
4
4
  export * from "./components/ActivityLog";
5
5
  export * from "./components/ai-assistant/AIAssistantLayout";
6
+ export * from "./components/Anchor";
6
7
  export * from "./components/AnimatePresence";
7
8
  export * from "./components/Avatar";
8
9
  export * from "./components/Banner";
@@ -61,6 +62,7 @@ export * from "./components/ResizableContainer";
61
62
  export * from "./components/RingProgress";
62
63
  export * from "./components/ScrollableSidebar";
63
64
  export * from "./components/ScrollArea";
65
+ export * from "./components/ScrollArea2";
64
66
  export * from "./components/SearchCompletionMenu";
65
67
  export * from "./components/Section";
66
68
  export * from "./components/SegmentedControl";
@@ -84,6 +86,7 @@ export * from "./components/TreeView";
84
86
  export * from "./components/UserPointer";
85
87
  export * from "./components/workspace/types";
86
88
  export * from "./components/workspace/WorkspaceLayout";
89
+ export * from "./components/workspace/WorkspaceSideContext";
87
90
  // Contexts
88
91
  export * from "./contexts/DesignSystemConfiguration";
89
92
  export * from "./contexts/DialogContext";
@@ -100,6 +103,7 @@ export * from "./hooks/useLabel";
100
103
  export * from "./hooks/usePlatform";
101
104
  export * from "./hooks/useTheme";
102
105
  export * from "./theme";
106
+ export * from "./theme/proseTheme";
103
107
  export * from "./utils/classNames";
104
108
  export * from "./utils/createSectionedMenu";
105
109
  export * from "./utils/formatByteSize";
@@ -0,0 +1,2 @@
1
+ export const proseTheme =
2
+ "n-prose n-prose-sm n-max-w-none [[data-theme='dark']_&]:n-prose-invert prose-a:n-text-blue-600";
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { chunkBy, partition } from "@noya-app/noya-utils";
4
- import { useSyncExternalStore } from "react";
4
+ import React, { useSyncExternalStore } from "react";
5
5
  import {
6
6
  isMenuItemSectionHeader,
7
7
  isNonSelectableMenuItem,
@@ -306,7 +306,11 @@ export function filterWithGroupedSections<T extends string>(
306
306
  );
307
307
  } else {
308
308
  const scoredItems = fuzzyFilter({
309
- items: selectableItems.map((item) => item.title.toString()),
309
+ items: selectableItems.map((item) => {
310
+ const title = item.title?.toString?.() ?? "";
311
+ const tooltipText = getNodeText(item.tooltip);
312
+ return tooltipText ? `${title} ${tooltipText}` : title;
313
+ }),
310
314
  query,
311
315
  });
312
316
 
@@ -389,3 +393,14 @@ export function useComboboxState<T extends string>(state: ComboboxState<T>) {
389
393
  state.getSnapshot
390
394
  );
391
395
  }
396
+
397
+ function getNodeText(node: React.ReactNode): string {
398
+ if (node === null || node === undefined || typeof node === "boolean")
399
+ return "";
400
+ if (typeof node === "string" || typeof node === "number") return String(node);
401
+ if (Array.isArray(node)) return node.map(getNodeText).join("");
402
+ if (React.isValidElement(node)) {
403
+ return getNodeText((node as any).props?.children);
404
+ }
405
+ return "";
406
+ }
@@ -0,0 +1,33 @@
1
+ const DETAIL_LINE_CLAMP_CLASS_MAP: Record<number, string> = {
2
+ 1: "n-truncate",
3
+ 2: "n-text-wrap n-line-clamp-2",
4
+ 3: "n-text-wrap n-line-clamp-3",
5
+ 4: "n-text-wrap n-line-clamp-4",
6
+ };
7
+
8
+ export function normalizeDetailLineClamp(
9
+ lineClamp?: number
10
+ ): number | undefined {
11
+ if (lineClamp == null) return undefined;
12
+
13
+ const normalized = Math.floor(lineClamp);
14
+
15
+ if (!Number.isFinite(normalized)) {
16
+ return undefined;
17
+ }
18
+
19
+ if (normalized < 1) return 1;
20
+ if (normalized > 4) return 4;
21
+
22
+ return normalized as 1 | 2 | 3 | 4;
23
+ }
24
+
25
+ export function getDetailLineClampClass(
26
+ lineClamp?: number
27
+ ): string | undefined {
28
+ const normalized = normalizeDetailLineClamp(lineClamp);
29
+
30
+ return normalized ? DETAIL_LINE_CLAMP_CLASS_MAP[normalized] : undefined;
31
+ }
32
+
33
+ export const DETAIL_LINE_CLAMP_CLASSES = DETAIL_LINE_CLAMP_CLASS_MAP;
@@ -1,11 +1,13 @@
1
1
  import noyaConfig from "@noya-app/noya-tailwind-config";
2
2
  import containerQueries from "@tailwindcss/container-queries";
3
+ import typography from "@tailwindcss/typography";
3
4
  import type { Config } from "tailwindcss";
4
5
 
5
6
  const config = {
6
7
  ...noyaConfig,
7
8
  prefix: "n-",
8
- plugins: [containerQueries],
9
+ plugins: [containerQueries, typography],
10
+ safelist: [...noyaConfig.safelist, "dark:n-prose-invert"],
9
11
  } satisfies Config;
10
12
 
11
13
  export default config;