@charcuterie/ui 3.23.0 → 3.24.0

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 (62) hide show
  1. package/dist/Board/Board.d.ts +6 -3
  2. package/dist/Board/Board.d.ts.map +1 -1
  3. package/dist/Board/Board.js +19 -1
  4. package/dist/Board/Board.js.map +1 -1
  5. package/dist/Board/BoardCard.d.ts +42 -7
  6. package/dist/Board/BoardCard.d.ts.map +1 -1
  7. package/dist/Board/BoardCard.js +54 -8
  8. package/dist/Board/BoardCard.js.map +1 -1
  9. package/dist/Nav/Nav.d.ts +167 -0
  10. package/dist/Nav/Nav.d.ts.map +1 -0
  11. package/dist/Nav/Nav.js +165 -0
  12. package/dist/Nav/Nav.js.map +1 -0
  13. package/dist/Nav/NavLink.d.ts +43 -0
  14. package/dist/Nav/NavLink.d.ts.map +1 -0
  15. package/dist/Nav/NavLink.js +111 -0
  16. package/dist/Nav/NavLink.js.map +1 -0
  17. package/dist/Nav/navItems.d.ts +67 -0
  18. package/dist/Nav/navItems.d.ts.map +1 -0
  19. package/dist/Nav/navItems.js +104 -0
  20. package/dist/Nav/navItems.js.map +1 -0
  21. package/dist/Nav/useNavLayout.d.ts +86 -0
  22. package/dist/Nav/useNavLayout.d.ts.map +1 -0
  23. package/dist/Nav/useNavLayout.js +114 -0
  24. package/dist/Nav/useNavLayout.js.map +1 -0
  25. package/dist/NavBar/NavBar.d.ts +37 -111
  26. package/dist/NavBar/NavBar.d.ts.map +1 -1
  27. package/dist/NavBar/NavBar.js +30 -110
  28. package/dist/NavBar/NavBar.js.map +1 -1
  29. package/dist/index.d.ts +6 -1
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +3 -1
  32. package/dist/index.js.map +1 -1
  33. package/package.json +1 -1
  34. package/src/Board/Board.mdx +25 -2
  35. package/src/Board/Board.stories.tsx +52 -1
  36. package/src/Board/Board.test.tsx +82 -0
  37. package/src/Board/Board.tsx +24 -4
  38. package/src/Board/BoardCard.tsx +103 -21
  39. package/src/Board/boardMoveHandleContainer.test.ts +54 -0
  40. package/src/Nav/Nav.mdx +169 -0
  41. package/src/Nav/Nav.stories.tsx +402 -0
  42. package/src/Nav/Nav.test.tsx +338 -0
  43. package/src/Nav/Nav.tsx +406 -0
  44. package/src/Nav/NavLink.tsx +220 -0
  45. package/src/Nav/navItems.test.ts +163 -0
  46. package/src/Nav/navItems.ts +175 -0
  47. package/src/Nav/useNavLayout.ts +197 -0
  48. package/src/NavBar/NavBar.mdx +26 -0
  49. package/src/NavBar/NavBar.tsx +54 -240
  50. package/src/index.ts +20 -1
  51. package/src/sourceRules.test.ts +13 -1
  52. package/dist/NavBar/NavBarLink.d.ts +0 -43
  53. package/dist/NavBar/NavBarLink.d.ts.map +0 -1
  54. package/dist/NavBar/NavBarLink.js +0 -67
  55. package/dist/NavBar/NavBarLink.js.map +0 -1
  56. package/dist/NavBar/navBarCurrent.d.ts +0 -37
  57. package/dist/NavBar/navBarCurrent.d.ts.map +0 -1
  58. package/dist/NavBar/navBarCurrent.js +0 -48
  59. package/dist/NavBar/navBarCurrent.js.map +0 -1
  60. package/src/NavBar/NavBarLink.tsx +0 -139
  61. package/src/NavBar/navBarCurrent.test.ts +0 -81
  62. package/src/NavBar/navBarCurrent.ts +0 -64
@@ -1,139 +0,0 @@
1
- import type { ControlSize } from "@charcuterie/tokens"
2
- import type { MouseEvent, ReactNode } from "react"
3
-
4
- import {
5
- CONTROL_SIZE_CLASS,
6
- PANEL_ITEM_SIZE_CLASS,
7
- } from "../controlStyles.ts"
8
- import { FOCUS_RING_CLASS } from "../intentStyles.ts"
9
- import { useRouterLink } from "../RouterLink/RouterLinkProvider.tsx"
10
- import { getIsRoutedHref } from "../RouterLink/routerLink.ts"
11
- import { toClassName } from "../toClassName.ts"
12
- import type { NavBarItem } from "./NavBar.tsx"
13
-
14
- export type NavBarLinkProps = {
15
- isCurrent: boolean
16
- item: NavBarItem
17
- /**
18
- * Fires when the reader activates this link **in a way that keeps
19
- * them on the page** — a plain left click, Enter, Space. It is
20
- * what closes the collapsed panel behind them.
21
- *
22
- * A modified click (ctrl, meta, shift, alt) opens a new tab and
23
- * leaves this document exactly where it was, so it deliberately
24
- * does not fire: shutting the menu the reader is still working
25
- * through is the behaviour that makes people stop ctrl-clicking.
26
- */
27
- onNavigate?: () => void
28
- /** Where it is drawn. A bar row and a panel row paint differently. */
29
- placement: "bar" | "panel"
30
- size: ControlSize
31
- /** Hands the bar row's box to the measuring hook. Bar only. */
32
- trackElement?: (
33
- key: string,
34
- element: HTMLElement | null,
35
- ) => void
36
- }
37
-
38
- /**
39
- * One destination, drawn either in the bar or in the collapsed
40
- * panel — and it is a real `<a href>` in both.
41
- *
42
- * That is the whole reason this component exists rather than the
43
- * `Menu` a collapsed `Toolbar` opens: `MenuItem` is `{ label,
44
- * onSelect }`, so a nav folded into one becomes a row of
45
- * `<button>`s. It looks identical in a screenshot and it loses
46
- * middle-click, ctrl-click, "open in a new tab", "copy link
47
- * address", the status bar, and the browser's own visited styling.
48
- * `BuildingAnApp.mdx` names `<button onClick={navigate}>` as the
49
- * fleet's most common navigation defect; a collapse mechanism that
50
- * introduced it at narrow widths would be that defect on a timer.
51
- *
52
- * The router seam is `ButtonLink`'s, unchanged: an external
53
- * destination or a fragment falls back to a plain anchor rather than
54
- * being pushed onto a history stack that has no route for it.
55
- */
56
- export const NavBarLink = ({
57
- isCurrent,
58
- item,
59
- onNavigate,
60
- placement,
61
- size,
62
- trackElement,
63
- }: NavBarLinkProps): ReactNode => {
64
- const RouterLink = useRouterLink()
65
-
66
- const LinkElement = getIsRoutedHref(item.href)
67
- ? RouterLink
68
- : "a"
69
-
70
- const className =
71
- placement === "bar"
72
- ? toClassName(
73
- "inline-flex shrink-0 items-center rounded-md border border-transparent whitespace-nowrap transition-colors duration-(--duration-fast) ease-standard",
74
- CONTROL_SIZE_CLASS[size],
75
- // Two written-out entries rather than one interpolated
76
- // string, the same rule `SegmentedOption` follows:
77
- // Tailwind's scanner cannot see a class that exists only
78
- // at runtime, and `tailwindCandidates.test.ts` rejects a
79
- // template literal in a className outright.
80
- isCurrent
81
- ? "bg-intent-neutral-surface font-medium text-content-primary"
82
- : "text-content-secondary hover:bg-intent-neutral-surface hover:text-content-primary",
83
- FOCUS_RING_CLASS,
84
- )
85
- : toClassName(
86
- "flex w-full items-center rounded-sm text-start transition-colors duration-(--duration-fast) ease-standard",
87
- PANEL_ITEM_SIZE_CLASS[size],
88
- // `-hover`, not the plain surface: the panel is drawn on
89
- // `surface-overlay`, which every dark scheme paints
90
- // *lighter* than the base tint, so the plain one reads as
91
- // no change at all. Same correction `MenuAction` carries.
92
- isCurrent
93
- ? "bg-intent-neutral-surface-hover font-medium text-content-primary"
94
- : "text-content-primary hover:bg-intent-neutral-surface-hover",
95
- FOCUS_RING_CLASS,
96
- )
97
-
98
- return (
99
- <LinkElement
100
- // The platform's own "you are here", and the reason the paint
101
- // above is never the only signal: a highlight is invisible to
102
- // a screen reader and to anyone who cannot tell the two
103
- // surfaces apart.
104
- aria-current={isCurrent ? "page" : undefined}
105
- className={className}
106
- href={item.href}
107
- onClick={(event: MouseEvent<HTMLAnchorElement>) => {
108
- if (
109
- event.defaultPrevented ||
110
- event.button !== 0 ||
111
- event.altKey ||
112
- event.ctrlKey ||
113
- event.metaKey ||
114
- event.shiftKey
115
- ) {
116
- return
117
- }
118
-
119
- onNavigate?.()
120
- }}
121
- ref={(element: HTMLAnchorElement | null) => {
122
- trackElement?.(item.key ?? item.href, element)
123
- }}
124
- >
125
- {item.icon === undefined ? null : (
126
- // Decoration beside the item's own text. An icon that
127
- // announces itself turns "Settings" into "gear Settings".
128
- <span
129
- aria-hidden="true"
130
- className="shrink-0 text-content-secondary"
131
- >
132
- {item.icon}
133
- </span>
134
- )}
135
-
136
- {item.label}
137
- </LinkElement>
138
- )
139
- }
@@ -1,81 +0,0 @@
1
- import { expect, test } from "vitest"
2
-
3
- import { getIsCurrentHref } from "./navBarCurrent.ts"
4
-
5
- const isCurrent = (currentHref: string, href: string) =>
6
- getIsCurrentHref({ currentHref, href })
7
-
8
- test("a destination is current on its own path", () => {
9
- expect(isCurrent("/backlog", "/backlog")).toBe(true)
10
-
11
- expect(isCurrent("/backlog", "/board")).toBe(false)
12
- })
13
-
14
- /**
15
- * A nav item names a **section**, and a section does not stop being
16
- * the one you are in when you open something inside it.
17
- */
18
- test("a parent path is current for its children", () => {
19
- expect(isCurrent("/tasks/41", "/tasks")).toBe(true)
20
-
21
- expect(isCurrent("/tasks/41/edit", "/tasks")).toBe(true)
22
- })
23
-
24
- /**
25
- * The match is on whole segments. A bare `startsWith` marks `/task`
26
- * as current for `/tasks`, which is a highlight on the wrong item
27
- * and nothing to see in a review.
28
- */
29
- test("a partial segment is not a match", () => {
30
- expect(isCurrent("/tasks", "/task")).toBe(false)
31
-
32
- expect(isCurrent("/backlogged", "/backlog")).toBe(false)
33
- })
34
-
35
- /**
36
- * react-router's `<NavLink to="/">` is active on every route unless
37
- * the caller remembers `end` — the footgun behind every "why is Home
38
- * always highlighted" bug. The root matches only itself here, and
39
- * there is no prop to change it.
40
- */
41
- test("the root is exact", () => {
42
- expect(isCurrent("/", "/")).toBe(true)
43
-
44
- expect(isCurrent("/backlog", "/")).toBe(false)
45
- })
46
-
47
- /**
48
- * Sorting a list does not move the reader to a different
49
- * destination.
50
- */
51
- test("the query string and the fragment are not part of the answer", () => {
52
- expect(
53
- isCurrent("/backlog?focus=search", "/backlog"),
54
- ).toBe(true)
55
-
56
- expect(isCurrent("/backlog#top", "/backlog")).toBe(true)
57
-
58
- expect(
59
- isCurrent("/backlog", "/backlog?focus=search"),
60
- ).toBe(true)
61
- })
62
-
63
- test("a trailing slash is the same place", () => {
64
- expect(isCurrent("/backlog/", "/backlog")).toBe(true)
65
-
66
- expect(isCurrent("/backlog", "/backlog/")).toBe(true)
67
- })
68
-
69
- /**
70
- * `undefined` is what a caller passes when it does not know, and the
71
- * honest reading of that is that nothing is current — not that
72
- * everything is.
73
- */
74
- test("no current path marks nothing", () => {
75
- expect(
76
- getIsCurrentHref({
77
- currentHref: undefined,
78
- href: "/backlog",
79
- }),
80
- ).toBe(false)
81
- })
@@ -1,64 +0,0 @@
1
- /**
2
- * Which destination the reader is on — the rule behind
3
- * `aria-current="page"`, kept pure so it can be checked in Node.
4
- *
5
- * The library is router-agnostic (see `RouterLinkProvider`), so it
6
- * cannot ask a router which link is active. The app hands it one
7
- * string — `useLocation().pathname`, `window.location.pathname`,
8
- * whatever it has — and this decides the rest.
9
- *
10
- * ## A parent path is current for its children
11
- *
12
- * `/tasks` is current while the reader is on `/tasks/41`, because a
13
- * nav item names a **section** and a section does not stop being the
14
- * one you are in when you open something inside it. The match is on
15
- * whole segments: `/task` is *not* current for `/tasks`, which a bare
16
- * `startsWith` would get wrong.
17
- *
18
- * ## `/` is exact, and that is deliberately not react-router
19
- *
20
- * react-router's `<NavLink to="/">` is active on **every** route
21
- * unless the caller remembers `end`, which is the footgun behind
22
- * every "why is Home always highlighted" bug in the fleet. The root
23
- * matches only itself here, and there is no prop to change it: a nav
24
- * with two current items is a nav that has stopped saying anything.
25
- *
26
- * ## The query string and the fragment are not part of the answer
27
- *
28
- * `/backlog?focus=search` is still the Backlog. Sorting a list does
29
- * not move the reader to a different destination.
30
- */
31
-
32
- const toComparablePath = (href: string): string => {
33
- const path = href.split(/[#?]/u)[0] ?? ""
34
-
35
- // A trailing slash is the same place, so it must not change the
36
- // answer — but `/` itself has to survive being trimmed.
37
- return path.length > 1 ? path.replace(/\/+$/u, "") : path
38
- }
39
-
40
- export const getIsCurrentHref = ({
41
- currentHref,
42
- href,
43
- }: {
44
- /** Where the reader is. `undefined` means "nothing is current". */
45
- currentHref: string | undefined
46
- /** Where this destination goes. */
47
- href: string
48
- }): boolean => {
49
- if (currentHref === undefined) {
50
- return false
51
- }
52
-
53
- const current = toComparablePath(currentHref)
54
-
55
- const target = toComparablePath(href)
56
-
57
- if (target === "" || target === "/") {
58
- return current === target
59
- }
60
-
61
- return (
62
- current === target || current.startsWith(`${target}/`)
63
- )
64
- }