@ably/ui 17.9.16 → 17.11.0-dev.4c4b6b55
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.
- package/AGENTS.md +197 -0
- package/core/Header/types.js +2 -0
- package/core/Header/types.js.map +1 -0
- package/core/Header.js +1 -1
- package/core/Header.js.map +1 -1
- package/core/hooks/use-themed-scrollpoints.js +2 -0
- package/core/hooks/use-themed-scrollpoints.js.map +1 -0
- package/core/hooks/use-themed-scrollpoints.test.js +2 -0
- package/core/hooks/use-themed-scrollpoints.test.js.map +1 -0
- package/index.d.ts +22 -2
- package/package.json +5 -2
package/AGENTS.md
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# Agent Development Guide
|
|
2
|
+
|
|
3
|
+
This file is not necessarily for people, the intended audience is automated agents.
|
|
4
|
+
|
|
5
|
+
## Other references
|
|
6
|
+
|
|
7
|
+
Consider the content of `README.md` as well, it contains technical information
|
|
8
|
+
used by contributors to this project, as well as consumers of this project
|
|
9
|
+
|
|
10
|
+
## Consumers
|
|
11
|
+
|
|
12
|
+
This project is intended to primarily be consumed by the Ably website, voltaire
|
|
13
|
+
& docs projects. It is distributed via NPM as the `@ably/ui` package.
|
|
14
|
+
|
|
15
|
+
## Build & Test Commands
|
|
16
|
+
|
|
17
|
+
- `pnpm build` - Build the library (prebuild, icons, swc, tsc, cleanup)
|
|
18
|
+
- `pnpm test` - Run all tests with Vitest
|
|
19
|
+
- `pnpm test -- src/core/insights/index.test.ts` - Run a single test file
|
|
20
|
+
- `pnpm lint` - Run ESLint on all files
|
|
21
|
+
- `pnpm format:check` - Check formatting with Prettier
|
|
22
|
+
- `pnpm format:write` - Auto-format all files with Prettier
|
|
23
|
+
- `pnpm storybook` - Start Storybook dev server on port 6006
|
|
24
|
+
- `pnpm start` - Start Vite dev server on port 5000
|
|
25
|
+
|
|
26
|
+
## Code Style
|
|
27
|
+
|
|
28
|
+
- **Language**: TypeScript with strict mode enabled
|
|
29
|
+
- **React**: Use functional components with hooks; React 18.x
|
|
30
|
+
- **Imports**: Default export for main component, named exports for types/utils
|
|
31
|
+
- **Naming**: PascalCase for components/types, camelCase for functions/variables,
|
|
32
|
+
kebab-case for files
|
|
33
|
+
- **Types**: Define prop types as `ComponentNameProps`, use `PropsWithChildren<T>`
|
|
34
|
+
when needed
|
|
35
|
+
- **Styling**: Tailwind 3.4.
|
|
36
|
+
- **Utility**: Use `cn()` from `./src/core/utils/cn` for className merging (clsx
|
|
37
|
+
& tailwind-merge)
|
|
38
|
+
- **Formatting**: Prettier defaults (no config = defaults), 2-space indent
|
|
39
|
+
- **Error Handling**: Wrap external service calls in try-catch, log with logger module
|
|
40
|
+
- **Comments**: JSDoc for props, inline comments for complex logic
|
|
41
|
+
|
|
42
|
+
Keep emojis in the code to a minimum, only introduce them if there is precedent
|
|
43
|
+
in the file you're working on.
|
|
44
|
+
|
|
45
|
+
Comments and commit messages should not include statements like "local tests pass",
|
|
46
|
+
this is a given for how we work.
|
|
47
|
+
|
|
48
|
+
## Development
|
|
49
|
+
|
|
50
|
+
- Run `pnpm lint` & `pnpm format:write` on files after making changes, we lint
|
|
51
|
+
files in CI and don't want preventable failures. `pnpm lint:fix` should also
|
|
52
|
+
apply our formatting rules while trying to fix most things for you
|
|
53
|
+
- Run tests with `pnpm test` after making file changes
|
|
54
|
+
|
|
55
|
+
## Styling Guide
|
|
56
|
+
|
|
57
|
+
### Color Palettes
|
|
58
|
+
|
|
59
|
+
The design system uses semantic color palettes defined in `src/core/styles/properties.css`
|
|
60
|
+
and configured for Tailwind in `tailwind.config.js`. Each palette has a different
|
|
61
|
+
number of color values:
|
|
62
|
+
|
|
63
|
+
- **Neutral**: 000, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300 (14 values)
|
|
64
|
+
- **Orange**: 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100 (11 values)
|
|
65
|
+
- **Yellow**: 100, 200, 300, 400, 500, 600, 700, 800, 900 (9 values)
|
|
66
|
+
- **Green**: 100, 200, 300, 400, 500, 600, 700, 800, 900 (9 values)
|
|
67
|
+
- **Blue**: 100, 200, 300, 400, 500, 600, 700, 800, 900 (9 values)
|
|
68
|
+
- **Violet**: 100, 200, 300, 400, 500, 600, 700, 800, 900 (9 values)
|
|
69
|
+
- **Pink**: 100, 200, 300, 400, 500, 600, 700, 800, 900 (9 values)
|
|
70
|
+
|
|
71
|
+
### Interactive Element Styling Patterns
|
|
72
|
+
|
|
73
|
+
When developing components with @ably/ui, **always** use Tailwind classes following
|
|
74
|
+
these established patterns to ensure consistent interactive behavior across light
|
|
75
|
+
and dark modes:
|
|
76
|
+
|
|
77
|
+
#### Dark Mode Mirroring
|
|
78
|
+
|
|
79
|
+
For any given color, add a dark mode class that mirrors it across the palette.
|
|
80
|
+
Lower values (lighter colors) in light mode should map to higher values (darker
|
|
81
|
+
colors) in dark mode, and vice versa.
|
|
82
|
+
|
|
83
|
+
**Examples:**
|
|
84
|
+
|
|
85
|
+
- `bg-neutral-100` pairs with `dark:bg-neutral-1200`
|
|
86
|
+
- `bg-neutral-200` pairs with `dark:bg-neutral-1100`
|
|
87
|
+
- `bg-neutral-1200` pairs with `dark:bg-neutral-100`
|
|
88
|
+
- `text-neutral-1300` pairs with `dark:text-neutral-000`
|
|
89
|
+
- `bg-orange-200` pairs with `dark:bg-orange-900` (orange has 11 values: 200 + 900 = 1100)
|
|
90
|
+
- `bg-blue-300` pairs with `dark:bg-blue-700` (blue has 9 values: 300 + 700 = 1000)
|
|
91
|
+
|
|
92
|
+
The sum of mirrored color numbers should equal the total palette range. Different
|
|
93
|
+
palettes have different ranges, so calculate mirrors accordingly:
|
|
94
|
+
|
|
95
|
+
- Neutral (000-1300): `light + dark = 1300`
|
|
96
|
+
- Orange (100-1100): `light + dark = 1200`
|
|
97
|
+
- Secondary colors (100-900): `light + dark = 1000`
|
|
98
|
+
|
|
99
|
+
#### Hover States
|
|
100
|
+
|
|
101
|
+
Use the **next color value** along the palette for hover states:
|
|
102
|
+
|
|
103
|
+
- `bg-neutral-100` → `hover:bg-neutral-200`
|
|
104
|
+
- `bg-neutral-200` → `hover:bg-neutral-300`
|
|
105
|
+
- `bg-orange-600` → `hover:bg-orange-700`
|
|
106
|
+
|
|
107
|
+
Apply this pattern to both light and dark mode classes:
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
bg-neutral-200 hover:bg-neutral-300
|
|
111
|
+
dark:bg-neutral-1100 dark:hover:bg-neutral-1000
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
#### Active States
|
|
115
|
+
|
|
116
|
+
Use **two color values** along the palette for active/pressed states:
|
|
117
|
+
|
|
118
|
+
- `bg-neutral-100` → `active:bg-neutral-300`
|
|
119
|
+
- `bg-neutral-200` → `active:bg-neutral-400`
|
|
120
|
+
- `bg-orange-600` → `active:bg-orange-800`
|
|
121
|
+
|
|
122
|
+
Apply to both modes:
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
bg-neutral-200 hover:bg-neutral-300 active:bg-neutral-400
|
|
126
|
+
dark:bg-neutral-1100 dark:hover:bg-neutral-1000 dark:active:bg-neutral-900
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
#### Focus Styles
|
|
130
|
+
|
|
131
|
+
Add the `focus-base` class to all interactive elements (buttons, links, inputs,
|
|
132
|
+
selects, etc.). This class is defined in `src/core/styles/utils.css` and provides
|
|
133
|
+
consistent focus styling with an accessible outline:
|
|
134
|
+
|
|
135
|
+
```css
|
|
136
|
+
.focus-base {
|
|
137
|
+
@apply focus:outline-none focus-visible:outline-4 focus-visible:outline-offset-0 focus-visible:outline-gui-focus;
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
#### Transitions
|
|
142
|
+
|
|
143
|
+
Add `transition-colors` to interactive elements unless a higher-specificity
|
|
144
|
+
`transition` class is already present (e.g., `transition-all`, `transition-transform`).
|
|
145
|
+
This ensures smooth visual feedback for state changes.
|
|
146
|
+
|
|
147
|
+
### Complete Example
|
|
148
|
+
|
|
149
|
+
Here's a complete button component demonstrating all patterns:
|
|
150
|
+
|
|
151
|
+
```tsx
|
|
152
|
+
<button
|
|
153
|
+
className={cn(
|
|
154
|
+
"px-4 py-2 rounded",
|
|
155
|
+
"bg-neutral-200 hover:bg-neutral-300 active:bg-neutral-400",
|
|
156
|
+
"dark:bg-neutral-1100 dark:hover:bg-neutral-1000 dark:active:bg-neutral-900",
|
|
157
|
+
"text-neutral-1300 dark:text-neutral-000",
|
|
158
|
+
"focus-base transition-colors",
|
|
159
|
+
)}
|
|
160
|
+
>
|
|
161
|
+
Click me
|
|
162
|
+
</button>
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Additional Examples
|
|
166
|
+
|
|
167
|
+
**Select dropdown:**
|
|
168
|
+
|
|
169
|
+
```tsx
|
|
170
|
+
<Select.Trigger
|
|
171
|
+
className="bg-neutral-200 hover:bg-neutral-300 active:bg-neutral-400 dark:bg-neutral-1100 dark:hover:bg-neutral-1000 dark:active:bg-neutral-900 focus-base transition-colors border border-neutral-300 dark:border-neutral-1000"
|
|
172
|
+
>
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**Badge with orange:**
|
|
176
|
+
|
|
177
|
+
```tsx
|
|
178
|
+
<span
|
|
179
|
+
className="bg-orange-200 hover:bg-orange-300 active:bg-orange-400 dark:bg-orange-900 dark:hover:bg-orange-800 dark:active:bg-orange-700 focus-base transition-colors"
|
|
180
|
+
>
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**Toggle/Switch:**
|
|
184
|
+
|
|
185
|
+
```tsx
|
|
186
|
+
<Switch
|
|
187
|
+
className="bg-neutral-600 hover:bg-neutral-700 active:bg-neutral-800 data-[state=checked]:bg-orange-600 data-[state=checked]:hover:bg-orange-700 data-[state=checked]:active:bg-orange-800 focus-base transition-colors"
|
|
188
|
+
>
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Git workflow
|
|
192
|
+
|
|
193
|
+
- Always do work on a new branch, start the branch on the HEAD of `origin/main`
|
|
194
|
+
- Before pushing the branch run the tests and linters to ensure they are happy
|
|
195
|
+
- When updating a branch, rebase on `origin/main` and force-push (with lease)
|
|
196
|
+
- Use our PR template in the `.github` folder as a reference for the pull request
|
|
197
|
+
- Keep commit messages concise
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/core/Header/types.ts"],"sourcesContent":["export type ThemedScrollpoint = {\n id: string;\n className: string;\n};\n"],"names":[],"mappings":"AAAA,QAGE"}
|
package/core/Header.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import React,{useState,useEffect,useRef,useMemo,useCallback}from"react";import Icon from"./Icon";import cn from"./utils/cn";import Logo from"./Logo";import{componentMaxHeight,HEADER_BOTTOM_MARGIN,HEADER_HEIGHT}from"./utils/heights";import{HeaderLinks}from"./Header/HeaderLinks";import{throttle}from"es-toolkit/compat";import{COLLAPSE_TRIGGER_DISTANCE}from"./Notice/component";const FLEXIBLE_DESKTOP_CLASSES="hidden md:flex flex-1 items-center h-full";const MAX_MOBILE_MENU_WIDTH="560px";const Header=({className,isNoticeBannerEnabled=false,noticeHeight=0,searchBar,searchButton,logoHref,headerLinks,headerLinksClassName,headerCenterClassName,nav,mobileNav,sessionState,themedScrollpoints=[],searchButtonVisibility="all",location,logoBadge})=>{const[showMenu,setShowMenu]=useState(false);const[fadingOut,setFadingOut]=useState(false);const[noticeBannerVisible,setNoticeBannerVisible]=useState(isNoticeBannerEnabled);const menuRef=useRef(null);const
|
|
1
|
+
import React,{useState,useEffect,useRef,useMemo,useCallback}from"react";import Icon from"./Icon";import cn from"./utils/cn";import Logo from"./Logo";import{componentMaxHeight,HEADER_BOTTOM_MARGIN,HEADER_HEIGHT}from"./utils/heights";import{HeaderLinks}from"./Header/HeaderLinks";import{throttle}from"es-toolkit/compat";import{COLLAPSE_TRIGGER_DISTANCE}from"./Notice/component";import{useThemedScrollpoints}from"./hooks/use-themed-scrollpoints";const FLEXIBLE_DESKTOP_CLASSES="hidden md:flex flex-1 items-center h-full";const MAX_MOBILE_MENU_WIDTH="560px";const Header=({className,isNoticeBannerEnabled=false,noticeHeight=0,searchBar,searchButton,logoHref,headerLinks,headerLinksClassName,headerCenterClassName,nav,mobileNav,sessionState,themedScrollpoints=[],searchButtonVisibility="all",location,logoBadge})=>{const[showMenu,setShowMenu]=useState(false);const[fadingOut,setFadingOut]=useState(false);const[noticeBannerVisible,setNoticeBannerVisible]=useState(isNoticeBannerEnabled);const menuRef=useRef(null);const scrollpointClasses=useThemedScrollpoints(themedScrollpoints);const headerStyle={height:HEADER_HEIGHT,top:noticeBannerVisible?`${noticeHeight}px`:"0"};const headerClassName=cn("fixed left-0 top-0 w-full z-50 bg-neutral-000 dark:bg-neutral-1300 border-b border-neutral-300 dark:border-neutral-1000 transition-all duration-300 ease-in-out px-6 lg:px-16",scrollpointClasses,{"md:top-auto":noticeBannerVisible});const closeMenu=()=>{setFadingOut(true);setTimeout(()=>{setShowMenu(false);setFadingOut(false)},150)};const handleNoticeClose=useCallback(()=>{setNoticeBannerVisible(false)},[]);useEffect(()=>{document.addEventListener("notice-closed",handleNoticeClose);return()=>document.removeEventListener("notice-closed",handleNoticeClose)},[handleNoticeClose]);useEffect(()=>{if(!isNoticeBannerEnabled){return}const noticeElement=document.querySelector('[data-id="ui-notice"]');if(!noticeElement){console.warn("Header: Notice element not found");return}let previousVisibility=noticeBannerVisible;const handleScroll=()=>{const scrollY=window.scrollY;const isNoticeHidden=noticeElement.classList.contains("ui-announcement-hidden");const shouldBeVisible=scrollY<=COLLAPSE_TRIGGER_DISTANCE&&!isNoticeHidden;if(shouldBeVisible!==previousVisibility){previousVisibility=shouldBeVisible;setNoticeBannerVisible(shouldBeVisible)}};const throttledHandleScroll=throttle(handleScroll,100);handleScroll();window.addEventListener("scroll",throttledHandleScroll,{passive:true});return()=>{window.removeEventListener("scroll",throttledHandleScroll)}},[isNoticeBannerEnabled,noticeBannerVisible]);useEffect(()=>{const handleResize=()=>{if(window.innerWidth>=1040){setShowMenu(false)}};window.addEventListener("resize",handleResize);return()=>window.removeEventListener("resize",handleResize)},[]);useEffect(()=>{if(showMenu){document.body.classList.add("overflow-hidden")}else{document.body.classList.remove("overflow-hidden")}return()=>{document.body.classList.remove("overflow-hidden")}},[showMenu]);useEffect(()=>{if(location&&showMenu){closeMenu()}},[location]);const wrappedSearchButton=useMemo(()=>searchButton?React.createElement("div",{className:"text-neutral-1300 dark:text-neutral-000 flex items-center"},searchButton):null,[searchButton]);return React.createElement(React.Fragment,null,React.createElement("header",{role:"banner",style:headerStyle,className:headerClassName},React.createElement("div",{className:cn("flex items-center h-full",className)},React.createElement("nav",{className:"flex flex-1 h-full items-center"},["light","dark"].map(theme=>React.createElement(Logo,{key:theme,href:logoHref,theme:theme,badge:logoBadge,additionalLinkAttrs:{className:cn("h-full focus-base rounded mr-4 lg:mr-8",{"flex dark:hidden":theme==="light","hidden dark:flex":theme==="dark"})}})),React.createElement("div",{className:FLEXIBLE_DESKTOP_CLASSES},nav)),React.createElement("div",{className:"flex md:hidden flex-1 items-center justify-end gap-6 h-full"},searchButtonVisibility!=="desktop"?wrappedSearchButton:null,React.createElement("button",{className:"cursor-pointer focus-base rounded flex items-center p-0",onClick:()=>setShowMenu(!showMenu),"aria-expanded":showMenu,"aria-controls":"mobile-menu","aria-label":"Toggle menu"},React.createElement(Icon,{name:showMenu?"icon-gui-x-mark-outline":"icon-gui-bars-3-outline",additionalCSS:"text-neutral-1300 dark:text-neutral-000",size:"1.5rem"}))),searchBar?React.createElement("div",{className:cn(FLEXIBLE_DESKTOP_CLASSES,"justify-center",headerCenterClassName)},searchBar):null,React.createElement(HeaderLinks,{className:cn(FLEXIBLE_DESKTOP_CLASSES,headerLinksClassName),headerLinks:headerLinks,sessionState:sessionState,searchButton:wrappedSearchButton,searchButtonVisibility:searchButtonVisibility}))),showMenu?React.createElement(React.Fragment,null,React.createElement("div",{className:cn("fixed inset-0 bg-neutral-1300 dark:bg-neutral-1300 z-40",{"animate-[fade-in-ten-percent_150ms_ease-in-out_forwards]":!fadingOut,"animate-[fade-out-ten-percent_150ms_ease-in-out_forwards]":fadingOut}),onClick:closeMenu,onKeyDown:e=>e.key==="Escape"&&closeMenu(),role:"presentation"}),React.createElement("div",{id:"mobile-menu",className:"md:hidden fixed flex flex-col top-[4.75rem] overflow-y-hidden mx-3 right-0 w-[calc(100%-24px)] bg-neutral-000 dark:bg-neutral-1300 rounded-2xl ui-shadow-lg-medium z-50",style:{maxWidth:MAX_MOBILE_MENU_WIDTH,maxHeight:componentMaxHeight(HEADER_HEIGHT,HEADER_BOTTOM_MARGIN)},ref:menuRef,role:"navigation"},mobileNav,React.createElement(HeaderLinks,{headerLinks:headerLinks,sessionState:sessionState}))):null)};export default Header;
|
|
2
2
|
//# sourceMappingURL=Header.js.map
|
package/core/Header.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/Header.tsx"],"sourcesContent":["import React, {\n useState,\n useEffect,\n useRef,\n ReactNode,\n useMemo,\n useCallback,\n} from \"react\";\nimport Icon from \"./Icon\";\nimport cn from \"./utils/cn\";\nimport Logo from \"./Logo\";\nimport {\n componentMaxHeight,\n HEADER_BOTTOM_MARGIN,\n HEADER_HEIGHT,\n} from \"./utils/heights\";\nimport { HeaderLinks } from \"./Header/HeaderLinks\";\nimport { throttle } from \"es-toolkit/compat\";\nimport { Theme } from \"./styles/colors/types\";\nimport { COLLAPSE_TRIGGER_DISTANCE } from \"./Notice/component\";\n\nexport type ThemedScrollpoint = {\n id: string;\n className: string;\n};\n\n/**\n * Represents the state of the user session in the header.\n */\nexport type HeaderSessionState = {\n /**\n * Indicates if the user is signed in.\n */\n signedIn: boolean;\n\n /**\n * Information required to log out the user.\n */\n logOut: {\n /**\n * Token used for logging out.\n */\n token: string;\n\n /**\n * URL to log out the user.\n */\n href: string;\n };\n\n /**\n * Name of the user's account.\n */\n accountName: string;\n};\n\n/**\n * Props for the Header component.\n */\nexport type HeaderProps = {\n /**\n * Optional classnames to add to the header\n */\n className?: string;\n /**\n * Indicates if the notice banner is enabled.\n */\n isNoticeBannerEnabled?: boolean;\n /**\n * Height of the notice banner in pixels.\n */\n noticeHeight?: number;\n /**\n * Optional search bar element.\n */\n searchBar?: ReactNode;\n\n /**\n * Optional search button element.\n */\n searchButton?: ReactNode;\n\n /**\n * URL for the logo link.\n */\n logoHref?: string;\n\n /**\n * Array of header links.\n */\n headerLinks?: {\n /**\n * URL for the link.\n */\n href: string;\n\n /**\n * Label for the link.\n */\n label: string;\n\n /**\n * Indicates if the link should open in a new tab.\n */\n external?: boolean;\n }[];\n\n /**\n * Optional classname for styling the header links container.\n */\n headerLinksClassName?: string;\n\n /**\n * Optional classname for styling the header center container.\n */\n headerCenterClassName?: string;\n\n /**\n * Optional desktop navigation element.\n */\n nav?: ReactNode;\n\n /**\n * Optional mobile navigation element.\n */\n mobileNav?: ReactNode;\n\n /**\n * State of the user session.\n */\n sessionState?: HeaderSessionState;\n\n /**\n * Array of themed scrollpoints. The header will change its appearance based on the scrollpoint in view.\n */\n themedScrollpoints?: ThemedScrollpoint[];\n\n /**\n * Visibility setting for the search button.\n * - \"all\": Visible on all devices.\n * - \"desktop\": Visible only on desktop devices.\n * - \"mobile\": Visible only on mobile devices.\n */\n searchButtonVisibility?: \"all\" | \"desktop\" | \"mobile\";\n\n /**\n * Optional location object to detect location changes.\n */\n location?: Location;\n\n /**\n * Optional badge text to display on the logo.\n */\n logoBadge?: string;\n};\n\nconst FLEXIBLE_DESKTOP_CLASSES = \"hidden md:flex flex-1 items-center h-full\";\n\n/**\n * Maximum width before the menu expanded into full width\n */\nconst MAX_MOBILE_MENU_WIDTH = \"560px\";\n\nconst Header: React.FC<HeaderProps> = ({\n className,\n isNoticeBannerEnabled = false,\n noticeHeight = 0,\n searchBar,\n searchButton,\n logoHref,\n headerLinks,\n headerLinksClassName,\n headerCenterClassName,\n nav,\n mobileNav,\n sessionState,\n themedScrollpoints = [],\n searchButtonVisibility = \"all\",\n location,\n logoBadge,\n}) => {\n const [showMenu, setShowMenu] = useState(false);\n const [fadingOut, setFadingOut] = useState(false);\n const [noticeBannerVisible, setNoticeBannerVisible] = useState(\n isNoticeBannerEnabled,\n );\n const menuRef = useRef<HTMLDivElement>(null);\n const [scrollpointClasses, setScrollpointClasses] = useState<string>(\n themedScrollpoints.length > 0 ? themedScrollpoints[0].className : \"\",\n );\n\n const headerStyle = {\n height: HEADER_HEIGHT,\n top: noticeBannerVisible ? `${noticeHeight}px` : \"0\",\n };\n\n const headerClassName = cn(\n \"fixed left-0 top-0 w-full z-50 bg-neutral-000 dark:bg-neutral-1300 border-b border-neutral-300 dark:border-neutral-1000 transition-all duration-300 ease-in-out px-6 lg:px-16\",\n scrollpointClasses,\n {\n \"md:top-auto\": noticeBannerVisible,\n },\n );\n\n const closeMenu = () => {\n setFadingOut(true);\n\n setTimeout(() => {\n setShowMenu(false);\n setFadingOut(false);\n }, 150);\n };\n\n const handleNoticeClose = useCallback(() => {\n setNoticeBannerVisible(false);\n }, []);\n\n useEffect(() => {\n document.addEventListener(\"notice-closed\", handleNoticeClose);\n return () =>\n document.removeEventListener(\"notice-closed\", handleNoticeClose);\n }, [handleNoticeClose]);\n\n useEffect(() => {\n const handleScroll = () => {\n const noticeElement = document.querySelector('[data-id=\"ui-notice\"]');\n const isNoticeClosedToBeHidden = noticeElement?.classList.contains(\n \"ui-announcement-hidden\",\n );\n setNoticeBannerVisible(\n window.scrollY <= COLLAPSE_TRIGGER_DISTANCE &&\n isNoticeBannerEnabled &&\n !isNoticeClosedToBeHidden,\n );\n for (const scrollpoint of themedScrollpoints) {\n const element = document.getElementById(scrollpoint.id);\n if (element) {\n const rect = element.getBoundingClientRect();\n if (rect.top <= HEADER_HEIGHT && rect.bottom >= HEADER_HEIGHT) {\n setScrollpointClasses(scrollpoint.className);\n return;\n }\n }\n }\n };\n\n const throttledHandleScroll = throttle(handleScroll, 100);\n\n handleScroll();\n\n window.addEventListener(\"scroll\", throttledHandleScroll);\n return () => window.removeEventListener(\"scroll\", throttledHandleScroll);\n }, [themedScrollpoints, isNoticeBannerEnabled]);\n\n useEffect(() => {\n const handleResize = () => {\n if (window.innerWidth >= 1040) {\n setShowMenu(false);\n }\n };\n window.addEventListener(\"resize\", handleResize);\n return () => window.removeEventListener(\"resize\", handleResize);\n }, []);\n\n useEffect(() => {\n if (showMenu) {\n document.body.classList.add(\"overflow-hidden\");\n } else {\n document.body.classList.remove(\"overflow-hidden\");\n }\n\n // Cleanup on unmount\n return () => {\n document.body.classList.remove(\"overflow-hidden\");\n };\n }, [showMenu]);\n\n // Close menu when location changes\n useEffect(() => {\n if (location && showMenu) {\n closeMenu();\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [location]);\n\n const wrappedSearchButton = useMemo(\n () =>\n searchButton ? (\n <div className=\"text-neutral-1300 dark:text-neutral-000 flex items-center\">\n {searchButton}\n </div>\n ) : null,\n [searchButton],\n );\n\n return (\n <>\n <header role=\"banner\" style={headerStyle} className={headerClassName}>\n <div className={cn(\"flex items-center h-full\", className)}>\n <nav className=\"flex flex-1 h-full items-center\">\n {([\"light\", \"dark\"] as Theme[]).map((theme) => (\n <Logo\n key={theme}\n href={logoHref}\n theme={theme}\n badge={logoBadge}\n additionalLinkAttrs={{\n className: cn(\"h-full focus-base rounded mr-4 lg:mr-8\", {\n \"flex dark:hidden\": theme === \"light\",\n \"hidden dark:flex\": theme === \"dark\",\n }),\n }}\n />\n ))}\n <div className={FLEXIBLE_DESKTOP_CLASSES}>{nav}</div>\n </nav>\n <div className=\"flex md:hidden flex-1 items-center justify-end gap-6 h-full\">\n {searchButtonVisibility !== \"desktop\" ? wrappedSearchButton : null}\n <button\n className=\"cursor-pointer focus-base rounded flex items-center p-0\"\n onClick={() => setShowMenu(!showMenu)}\n aria-expanded={showMenu}\n aria-controls=\"mobile-menu\"\n aria-label=\"Toggle menu\"\n >\n <Icon\n name={\n showMenu\n ? \"icon-gui-x-mark-outline\"\n : \"icon-gui-bars-3-outline\"\n }\n additionalCSS=\"text-neutral-1300 dark:text-neutral-000\"\n size=\"1.5rem\"\n />\n </button>\n </div>\n {searchBar ? (\n <div\n className={cn(\n FLEXIBLE_DESKTOP_CLASSES,\n \"justify-center\",\n headerCenterClassName,\n )}\n >\n {searchBar}\n </div>\n ) : null}\n <HeaderLinks\n className={cn(FLEXIBLE_DESKTOP_CLASSES, headerLinksClassName)}\n headerLinks={headerLinks}\n sessionState={sessionState}\n searchButton={wrappedSearchButton}\n searchButtonVisibility={searchButtonVisibility}\n />\n </div>\n </header>\n {showMenu ? (\n <>\n <div\n className={cn(\n \"fixed inset-0 bg-neutral-1300 dark:bg-neutral-1300 z-40\",\n {\n \"animate-[fade-in-ten-percent_150ms_ease-in-out_forwards]\":\n !fadingOut,\n \"animate-[fade-out-ten-percent_150ms_ease-in-out_forwards]\":\n fadingOut,\n },\n )}\n onClick={closeMenu}\n onKeyDown={(e) => e.key === \"Escape\" && closeMenu()}\n role=\"presentation\"\n />\n <div\n id=\"mobile-menu\"\n className=\"md:hidden fixed flex flex-col top-[4.75rem] overflow-y-hidden mx-3 right-0 w-[calc(100%-24px)] bg-neutral-000 dark:bg-neutral-1300 rounded-2xl ui-shadow-lg-medium z-50\"\n style={{\n maxWidth: MAX_MOBILE_MENU_WIDTH,\n maxHeight: componentMaxHeight(\n HEADER_HEIGHT,\n HEADER_BOTTOM_MARGIN,\n ),\n }}\n ref={menuRef}\n role=\"navigation\"\n >\n {mobileNav}\n <HeaderLinks\n headerLinks={headerLinks}\n sessionState={sessionState}\n />\n </div>\n </>\n ) : null}\n </>\n );\n};\n\nexport default Header;\n"],"names":["React","useState","useEffect","useRef","useMemo","useCallback","Icon","cn","Logo","componentMaxHeight","HEADER_BOTTOM_MARGIN","HEADER_HEIGHT","HeaderLinks","throttle","COLLAPSE_TRIGGER_DISTANCE","FLEXIBLE_DESKTOP_CLASSES","MAX_MOBILE_MENU_WIDTH","Header","className","isNoticeBannerEnabled","noticeHeight","searchBar","searchButton","logoHref","headerLinks","headerLinksClassName","headerCenterClassName","nav","mobileNav","sessionState","themedScrollpoints","searchButtonVisibility","location","logoBadge","showMenu","setShowMenu","fadingOut","setFadingOut","noticeBannerVisible","setNoticeBannerVisible","menuRef","scrollpointClasses","setScrollpointClasses","length","headerStyle","height","top","headerClassName","closeMenu","setTimeout","handleNoticeClose","document","addEventListener","removeEventListener","handleScroll","noticeElement","querySelector","isNoticeClosedToBeHidden","classList","contains","window","scrollY","scrollpoint","element","getElementById","id","rect","getBoundingClientRect","bottom","throttledHandleScroll","handleResize","innerWidth","body","add","remove","wrappedSearchButton","div","header","role","style","map","theme","key","href","badge","additionalLinkAttrs","button","onClick","aria-expanded","aria-controls","aria-label","name","additionalCSS","size","onKeyDown","e","maxWidth","maxHeight","ref"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/core/Header.tsx"],"sourcesContent":["import React, {\n useState,\n useEffect,\n useRef,\n ReactNode,\n useMemo,\n useCallback,\n} from \"react\";\nimport Icon from \"./Icon\";\nimport cn from \"./utils/cn\";\nimport Logo from \"./Logo\";\nimport {\n componentMaxHeight,\n HEADER_BOTTOM_MARGIN,\n HEADER_HEIGHT,\n} from \"./utils/heights\";\nimport { HeaderLinks } from \"./Header/HeaderLinks\";\nimport { throttle } from \"es-toolkit/compat\";\nimport { Theme } from \"./styles/colors/types\";\nimport { COLLAPSE_TRIGGER_DISTANCE } from \"./Notice/component\";\nimport { useThemedScrollpoints } from \"./hooks/use-themed-scrollpoints\";\nimport { ThemedScrollpoint } from \"./Header/types\";\n\nexport type { ThemedScrollpoint };\n\n/**\n * Represents the state of the user session in the header.\n */\nexport type HeaderSessionState = {\n /**\n * Indicates if the user is signed in.\n */\n signedIn: boolean;\n\n /**\n * Information required to log out the user.\n */\n logOut: {\n /**\n * Token used for logging out.\n */\n token: string;\n\n /**\n * URL to log out the user.\n */\n href: string;\n };\n\n /**\n * Name of the user's account.\n */\n accountName: string;\n};\n\n/**\n * Props for the Header component.\n */\nexport type HeaderProps = {\n /**\n * Optional classnames to add to the header\n */\n className?: string;\n /**\n * Indicates if the notice banner is enabled.\n */\n isNoticeBannerEnabled?: boolean;\n /**\n * Height of the notice banner in pixels.\n */\n noticeHeight?: number;\n /**\n * Optional search bar element.\n */\n searchBar?: ReactNode;\n\n /**\n * Optional search button element.\n */\n searchButton?: ReactNode;\n\n /**\n * URL for the logo link.\n */\n logoHref?: string;\n\n /**\n * Array of header links.\n */\n headerLinks?: {\n /**\n * URL for the link.\n */\n href: string;\n\n /**\n * Label for the link.\n */\n label: string;\n\n /**\n * Indicates if the link should open in a new tab.\n */\n external?: boolean;\n }[];\n\n /**\n * Optional classname for styling the header links container.\n */\n headerLinksClassName?: string;\n\n /**\n * Optional classname for styling the header center container.\n */\n headerCenterClassName?: string;\n\n /**\n * Optional desktop navigation element.\n */\n nav?: ReactNode;\n\n /**\n * Optional mobile navigation element.\n */\n mobileNav?: ReactNode;\n\n /**\n * State of the user session.\n */\n sessionState?: HeaderSessionState;\n\n /**\n * Array of themed scrollpoints. The header will change its appearance based on the scrollpoint in view.\n */\n themedScrollpoints?: ThemedScrollpoint[];\n\n /**\n * Visibility setting for the search button.\n * - \"all\": Visible on all devices.\n * - \"desktop\": Visible only on desktop devices.\n * - \"mobile\": Visible only on mobile devices.\n */\n searchButtonVisibility?: \"all\" | \"desktop\" | \"mobile\";\n\n /**\n * Optional location object to detect location changes.\n */\n location?: Location;\n\n /**\n * Optional badge text to display on the logo.\n */\n logoBadge?: string;\n};\n\nconst FLEXIBLE_DESKTOP_CLASSES = \"hidden md:flex flex-1 items-center h-full\";\n\n/**\n * Maximum width before the menu expanded into full width\n */\nconst MAX_MOBILE_MENU_WIDTH = \"560px\";\n\nconst Header: React.FC<HeaderProps> = ({\n className,\n isNoticeBannerEnabled = false,\n noticeHeight = 0,\n searchBar,\n searchButton,\n logoHref,\n headerLinks,\n headerLinksClassName,\n headerCenterClassName,\n nav,\n mobileNav,\n sessionState,\n themedScrollpoints = [],\n searchButtonVisibility = \"all\",\n location,\n logoBadge,\n}) => {\n const [showMenu, setShowMenu] = useState(false);\n const [fadingOut, setFadingOut] = useState(false);\n const [noticeBannerVisible, setNoticeBannerVisible] = useState(\n isNoticeBannerEnabled,\n );\n const menuRef = useRef<HTMLDivElement>(null);\n const scrollpointClasses = useThemedScrollpoints(themedScrollpoints);\n\n const headerStyle = {\n height: HEADER_HEIGHT,\n top: noticeBannerVisible ? `${noticeHeight}px` : \"0\",\n };\n\n const headerClassName = cn(\n \"fixed left-0 top-0 w-full z-50 bg-neutral-000 dark:bg-neutral-1300 border-b border-neutral-300 dark:border-neutral-1000 transition-all duration-300 ease-in-out px-6 lg:px-16\",\n scrollpointClasses,\n {\n \"md:top-auto\": noticeBannerVisible,\n },\n );\n\n const closeMenu = () => {\n setFadingOut(true);\n\n setTimeout(() => {\n setShowMenu(false);\n setFadingOut(false);\n }, 150);\n };\n\n const handleNoticeClose = useCallback(() => {\n setNoticeBannerVisible(false);\n }, []);\n\n useEffect(() => {\n document.addEventListener(\"notice-closed\", handleNoticeClose);\n return () =>\n document.removeEventListener(\"notice-closed\", handleNoticeClose);\n }, [handleNoticeClose]);\n\n useEffect(() => {\n if (!isNoticeBannerEnabled) {\n return;\n }\n\n const noticeElement = document.querySelector('[data-id=\"ui-notice\"]');\n\n if (!noticeElement) {\n console.warn('Header: Notice element not found');\n return;\n }\n\n let previousVisibility = noticeBannerVisible;\n\n const handleScroll = () => {\n const scrollY = window.scrollY;\n const isNoticeHidden = noticeElement.classList.contains(\"ui-announcement-hidden\");\n\n const shouldBeVisible =\n scrollY <= COLLAPSE_TRIGGER_DISTANCE &&\n !isNoticeHidden;\n\n if (shouldBeVisible !== previousVisibility) {\n previousVisibility = shouldBeVisible;\n setNoticeBannerVisible(shouldBeVisible);\n }\n };\n\n const throttledHandleScroll = throttle(handleScroll, 100);\n\n handleScroll();\n\n window.addEventListener(\"scroll\", throttledHandleScroll, { passive: true });\n\n return () => {\n window.removeEventListener(\"scroll\", throttledHandleScroll);\n };\n }, [isNoticeBannerEnabled, noticeBannerVisible]);\n\n useEffect(() => {\n const handleResize = () => {\n if (window.innerWidth >= 1040) {\n setShowMenu(false);\n }\n };\n window.addEventListener(\"resize\", handleResize);\n return () => window.removeEventListener(\"resize\", handleResize);\n }, []);\n\n useEffect(() => {\n if (showMenu) {\n document.body.classList.add(\"overflow-hidden\");\n } else {\n document.body.classList.remove(\"overflow-hidden\");\n }\n\n // Cleanup on unmount\n return () => {\n document.body.classList.remove(\"overflow-hidden\");\n };\n }, [showMenu]);\n\n // Close menu when location changes\n useEffect(() => {\n if (location && showMenu) {\n closeMenu();\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [location]);\n\n const wrappedSearchButton = useMemo(\n () =>\n searchButton ? (\n <div className=\"text-neutral-1300 dark:text-neutral-000 flex items-center\">\n {searchButton}\n </div>\n ) : null,\n [searchButton],\n );\n\n return (\n <>\n <header role=\"banner\" style={headerStyle} className={headerClassName}>\n <div className={cn(\"flex items-center h-full\", className)}>\n <nav className=\"flex flex-1 h-full items-center\">\n {([\"light\", \"dark\"] as Theme[]).map((theme) => (\n <Logo\n key={theme}\n href={logoHref}\n theme={theme}\n badge={logoBadge}\n additionalLinkAttrs={{\n className: cn(\"h-full focus-base rounded mr-4 lg:mr-8\", {\n \"flex dark:hidden\": theme === \"light\",\n \"hidden dark:flex\": theme === \"dark\",\n }),\n }}\n />\n ))}\n <div className={FLEXIBLE_DESKTOP_CLASSES}>{nav}</div>\n </nav>\n <div className=\"flex md:hidden flex-1 items-center justify-end gap-6 h-full\">\n {searchButtonVisibility !== \"desktop\" ? wrappedSearchButton : null}\n <button\n className=\"cursor-pointer focus-base rounded flex items-center p-0\"\n onClick={() => setShowMenu(!showMenu)}\n aria-expanded={showMenu}\n aria-controls=\"mobile-menu\"\n aria-label=\"Toggle menu\"\n >\n <Icon\n name={\n showMenu\n ? \"icon-gui-x-mark-outline\"\n : \"icon-gui-bars-3-outline\"\n }\n additionalCSS=\"text-neutral-1300 dark:text-neutral-000\"\n size=\"1.5rem\"\n />\n </button>\n </div>\n {searchBar ? (\n <div\n className={cn(\n FLEXIBLE_DESKTOP_CLASSES,\n \"justify-center\",\n headerCenterClassName,\n )}\n >\n {searchBar}\n </div>\n ) : null}\n <HeaderLinks\n className={cn(FLEXIBLE_DESKTOP_CLASSES, headerLinksClassName)}\n headerLinks={headerLinks}\n sessionState={sessionState}\n searchButton={wrappedSearchButton}\n searchButtonVisibility={searchButtonVisibility}\n />\n </div>\n </header>\n {showMenu ? (\n <>\n <div\n className={cn(\n \"fixed inset-0 bg-neutral-1300 dark:bg-neutral-1300 z-40\",\n {\n \"animate-[fade-in-ten-percent_150ms_ease-in-out_forwards]\":\n !fadingOut,\n \"animate-[fade-out-ten-percent_150ms_ease-in-out_forwards]\":\n fadingOut,\n },\n )}\n onClick={closeMenu}\n onKeyDown={(e) => e.key === \"Escape\" && closeMenu()}\n role=\"presentation\"\n />\n <div\n id=\"mobile-menu\"\n className=\"md:hidden fixed flex flex-col top-[4.75rem] overflow-y-hidden mx-3 right-0 w-[calc(100%-24px)] bg-neutral-000 dark:bg-neutral-1300 rounded-2xl ui-shadow-lg-medium z-50\"\n style={{\n maxWidth: MAX_MOBILE_MENU_WIDTH,\n maxHeight: componentMaxHeight(\n HEADER_HEIGHT,\n HEADER_BOTTOM_MARGIN,\n ),\n }}\n ref={menuRef}\n role=\"navigation\"\n >\n {mobileNav}\n <HeaderLinks\n headerLinks={headerLinks}\n sessionState={sessionState}\n />\n </div>\n </>\n ) : null}\n </>\n );\n};\n\nexport default Header;\n"],"names":["React","useState","useEffect","useRef","useMemo","useCallback","Icon","cn","Logo","componentMaxHeight","HEADER_BOTTOM_MARGIN","HEADER_HEIGHT","HeaderLinks","throttle","COLLAPSE_TRIGGER_DISTANCE","useThemedScrollpoints","FLEXIBLE_DESKTOP_CLASSES","MAX_MOBILE_MENU_WIDTH","Header","className","isNoticeBannerEnabled","noticeHeight","searchBar","searchButton","logoHref","headerLinks","headerLinksClassName","headerCenterClassName","nav","mobileNav","sessionState","themedScrollpoints","searchButtonVisibility","location","logoBadge","showMenu","setShowMenu","fadingOut","setFadingOut","noticeBannerVisible","setNoticeBannerVisible","menuRef","scrollpointClasses","headerStyle","height","top","headerClassName","closeMenu","setTimeout","handleNoticeClose","document","addEventListener","removeEventListener","noticeElement","querySelector","console","warn","previousVisibility","handleScroll","scrollY","window","isNoticeHidden","classList","contains","shouldBeVisible","throttledHandleScroll","passive","handleResize","innerWidth","body","add","remove","wrappedSearchButton","div","header","role","style","map","theme","key","href","badge","additionalLinkAttrs","button","onClick","aria-expanded","aria-controls","aria-label","name","additionalCSS","size","onKeyDown","e","id","maxWidth","maxHeight","ref"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{useState,useEffect,useRef}from"react";const HEADER_HEIGHT=64;export function useThemedScrollpoints(scrollpoints){const[activeClassName,setActiveClassName]=useState(scrollpoints.length>0?scrollpoints[0].className:"");const previousClassNameRef=useRef(activeClassName);const observerRef=useRef(null);useEffect(()=>{if(scrollpoints.length===0){return}observerRef.current=new IntersectionObserver(entries=>{requestAnimationFrame(()=>{for(const entry of entries){if(entry.isIntersecting){const scrollpoint=scrollpoints.find(sp=>sp.id===entry.target.id);if(scrollpoint&&scrollpoint.className!==previousClassNameRef.current){previousClassNameRef.current=scrollpoint.className;setActiveClassName(scrollpoint.className);return}}}})},{rootMargin:`-${HEADER_HEIGHT}px 0px 0px 0px`,threshold:0});scrollpoints.forEach(({id})=>{const element=document.getElementById(id);if(element){observerRef.current?.observe(element)}else{console.warn(`useThemedScrollpoints: Element with id "${id}" not found in DOM`)}});return()=>{observerRef.current?.disconnect();observerRef.current=null}},[scrollpoints]);return activeClassName}
|
|
2
|
+
//# sourceMappingURL=use-themed-scrollpoints.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/core/hooks/use-themed-scrollpoints.ts"],"sourcesContent":["import { useState, useEffect, useRef } from \"react\";\nimport { ThemedScrollpoint } from \"../Header/types\";\n\nconst HEADER_HEIGHT = 64;\n\nexport function useThemedScrollpoints(\n scrollpoints: ThemedScrollpoint[]\n): string {\n const [activeClassName, setActiveClassName] = useState<string>(\n scrollpoints.length > 0 ? scrollpoints[0].className : \"\"\n );\n\n const previousClassNameRef = useRef<string>(activeClassName);\n const observerRef = useRef<IntersectionObserver | null>(null);\n\n useEffect(() => {\n if (scrollpoints.length === 0) {\n return;\n }\n\n observerRef.current = new IntersectionObserver(\n (entries) => {\n requestAnimationFrame(() => {\n for (const entry of entries) {\n if (entry.isIntersecting) {\n const scrollpoint = scrollpoints.find(\n (sp) => sp.id === entry.target.id\n );\n\n if (scrollpoint && scrollpoint.className !== previousClassNameRef.current) {\n previousClassNameRef.current = scrollpoint.className;\n setActiveClassName(scrollpoint.className);\n return;\n }\n }\n }\n });\n },\n {\n rootMargin: `-${HEADER_HEIGHT}px 0px 0px 0px`,\n threshold: 0,\n }\n );\n\n scrollpoints.forEach(({ id }) => {\n const element = document.getElementById(id);\n if (element) {\n observerRef.current?.observe(element);\n } else {\n console.warn(\n `useThemedScrollpoints: Element with id \"${id}\" not found in DOM`\n );\n }\n });\n\n return () => {\n observerRef.current?.disconnect();\n observerRef.current = null;\n };\n }, [scrollpoints]);\n\n return activeClassName;\n}\n"],"names":["useState","useEffect","useRef","HEADER_HEIGHT","useThemedScrollpoints","scrollpoints","activeClassName","setActiveClassName","length","className","previousClassNameRef","observerRef","current","IntersectionObserver","entries","requestAnimationFrame","entry","isIntersecting","scrollpoint","find","sp","id","target","rootMargin","threshold","forEach","element","document","getElementById","observe","console","warn","disconnect"],"mappings":"AAAA,OAASA,QAAQ,CAAEC,SAAS,CAAEC,MAAM,KAAQ,OAAQ,CAGpD,MAAMC,cAAgB,EAEtB,QAAO,SAASC,sBACdC,YAAiC,EAEjC,KAAM,CAACC,gBAAiBC,mBAAmB,CAAGP,SAC5CK,aAAaG,MAAM,CAAG,EAAIH,YAAY,CAAC,EAAE,CAACI,SAAS,CAAG,IAGxD,MAAMC,qBAAuBR,OAAeI,iBAC5C,MAAMK,YAAcT,OAAoC,MAExDD,UAAU,KACR,GAAII,aAAaG,MAAM,GAAK,EAAG,CAC7B,MACF,CAEAG,YAAYC,OAAO,CAAG,IAAIC,qBACxB,AAACC,UACCC,sBAAsB,KACpB,IAAK,MAAMC,SAASF,QAAS,CAC3B,GAAIE,MAAMC,cAAc,CAAE,CACxB,MAAMC,YAAcb,aAAac,IAAI,CACnC,AAACC,IAAOA,GAAGC,EAAE,GAAKL,MAAMM,MAAM,CAACD,EAAE,EAGnC,GAAIH,aAAeA,YAAYT,SAAS,GAAKC,qBAAqBE,OAAO,CAAE,CACzEF,qBAAqBE,OAAO,CAAGM,YAAYT,SAAS,CACpDF,mBAAmBW,YAAYT,SAAS,EACxC,MACF,CACF,CACF,CACF,EACF,EACA,CACEc,WAAY,CAAC,CAAC,EAAEpB,cAAc,cAAc,CAAC,CAC7CqB,UAAW,CACb,GAGFnB,aAAaoB,OAAO,CAAC,CAAC,CAAEJ,EAAE,CAAE,IAC1B,MAAMK,QAAUC,SAASC,cAAc,CAACP,IACxC,GAAIK,QAAS,CACXf,YAAYC,OAAO,EAAEiB,QAAQH,QAC/B,KAAO,CACLI,QAAQC,IAAI,CACV,CAAC,wCAAwC,EAAEV,GAAG,kBAAkB,CAAC,CAErE,CACF,GAEA,MAAO,KACLV,YAAYC,OAAO,EAAEoB,YACrBrB,CAAAA,YAAYC,OAAO,CAAG,IACxB,CACF,EAAG,CAACP,aAAa,EAEjB,OAAOC,eACT"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{describe,it,expect,beforeEach,afterEach,vi}from"vitest";import{renderHook,waitFor}from"@testing-library/react";import{useThemedScrollpoints}from"./use-themed-scrollpoints";describe("useThemedScrollpoints",()=>{let observerCallback;let mockObserve;let mockUnobserve;let mockDisconnect;beforeEach(()=>{mockObserve=vi.fn();mockUnobserve=vi.fn();mockDisconnect=vi.fn();global.IntersectionObserver=vi.fn(callback=>{observerCallback=callback;return{observe:mockObserve,unobserve:mockUnobserve,disconnect:mockDisconnect}});global.requestAnimationFrame=vi.fn(cb=>{cb(0);return 0})});afterEach(()=>{vi.clearAllMocks();document.body.innerHTML=""});it("returns first scrollpoint className on mount",()=>{const scrollpoints=[{id:"zone1",className:"theme-light"},{id:"zone2",className:"theme-dark"}];const{result}=renderHook(()=>useThemedScrollpoints(scrollpoints));expect(result.current).toBe("theme-light")});it("returns empty string when no scrollpoints provided",()=>{const{result}=renderHook(()=>useThemedScrollpoints([]));expect(result.current).toBe("")});it("does not create IntersectionObserver when no scrollpoints provided",()=>{renderHook(()=>useThemedScrollpoints([]));expect(IntersectionObserver).not.toHaveBeenCalled()});it("creates IntersectionObserver with correct config",()=>{const scrollpoints=[{id:"zone1",className:"theme-light"}];const elem=document.createElement("div");elem.id="zone1";document.body.appendChild(elem);renderHook(()=>useThemedScrollpoints(scrollpoints));expect(IntersectionObserver).toHaveBeenCalledWith(expect.any(Function),expect.objectContaining({rootMargin:"-64px 0px 0px 0px",threshold:0}))});it("observes all scrollpoint elements that exist in DOM",()=>{const elem1=document.createElement("div");elem1.id="zone1";const elem2=document.createElement("div");elem2.id="zone2";document.body.appendChild(elem1);document.body.appendChild(elem2);const scrollpoints=[{id:"zone1",className:"theme-light"},{id:"zone2",className:"theme-dark"}];renderHook(()=>useThemedScrollpoints(scrollpoints));expect(mockObserve).toHaveBeenCalledTimes(2);expect(mockObserve).toHaveBeenCalledWith(elem1);expect(mockObserve).toHaveBeenCalledWith(elem2)});it("logs warning for missing DOM elements",()=>{const consoleWarn=vi.spyOn(console,"warn").mockImplementation(()=>{});const scrollpoints=[{id:"non-existent",className:"theme"}];renderHook(()=>useThemedScrollpoints(scrollpoints));expect(consoleWarn).toHaveBeenCalledWith(expect.stringContaining('Element with id "non-existent" not found'));consoleWarn.mockRestore()});it("observes existing elements and warns about missing ones",()=>{const consoleWarn=vi.spyOn(console,"warn").mockImplementation(()=>{});const elem=document.createElement("div");elem.id="zone1";document.body.appendChild(elem);const scrollpoints=[{id:"zone1",className:"theme-light"},{id:"missing",className:"theme-dark"}];renderHook(()=>useThemedScrollpoints(scrollpoints));expect(mockObserve).toHaveBeenCalledTimes(1);expect(mockObserve).toHaveBeenCalledWith(elem);expect(consoleWarn).toHaveBeenCalledWith(expect.stringContaining('Element with id "missing" not found'));consoleWarn.mockRestore()});it("updates className when intersection occurs",async()=>{const elem1=document.createElement("div");elem1.id="zone1";const elem2=document.createElement("div");elem2.id="zone2";document.body.appendChild(elem1);document.body.appendChild(elem2);const scrollpoints=[{id:"zone1",className:"theme-light"},{id:"zone2",className:"theme-dark"}];const{result}=renderHook(()=>useThemedScrollpoints(scrollpoints));expect(result.current).toBe("theme-light");observerCallback([{target:elem2,isIntersecting:true}],{});await waitFor(()=>{expect(result.current).toBe("theme-dark")})});it("does not update className if same scrollpoint intersects again",async()=>{const elem=document.createElement("div");elem.id="zone1";document.body.appendChild(elem);const scrollpoints=[{id:"zone1",className:"theme-light"}];const{result}=renderHook(()=>useThemedScrollpoints(scrollpoints));expect(result.current).toBe("theme-light");const renderCount=result.current;observerCallback([{target:elem,isIntersecting:true}],{});expect(result.current).toBe(renderCount)});it("ignores non-intersecting entries",async()=>{const elem1=document.createElement("div");elem1.id="zone1";const elem2=document.createElement("div");elem2.id="zone2";document.body.appendChild(elem1);document.body.appendChild(elem2);const scrollpoints=[{id:"zone1",className:"theme-light"},{id:"zone2",className:"theme-dark"}];const{result}=renderHook(()=>useThemedScrollpoints(scrollpoints));expect(result.current).toBe("theme-light");observerCallback([{target:elem2,isIntersecting:false}],{});expect(result.current).toBe("theme-light")});it("uses first intersecting entry when multiple entries intersect",async()=>{const elem1=document.createElement("div");elem1.id="zone1";const elem2=document.createElement("div");elem2.id="zone2";const elem3=document.createElement("div");elem3.id="zone3";document.body.appendChild(elem1);document.body.appendChild(elem2);document.body.appendChild(elem3);const scrollpoints=[{id:"zone1",className:"theme-light"},{id:"zone2",className:"theme-dark"},{id:"zone3",className:"theme-blue"}];const{result}=renderHook(()=>useThemedScrollpoints(scrollpoints));observerCallback([{target:elem2,isIntersecting:true},{target:elem3,isIntersecting:true}],{});await waitFor(()=>{expect(result.current).toBe("theme-dark")})});it("disconnects observer on unmount",()=>{const elem=document.createElement("div");elem.id="zone1";document.body.appendChild(elem);const scrollpoints=[{id:"zone1",className:"theme-light"}];const{unmount}=renderHook(()=>useThemedScrollpoints(scrollpoints));unmount();expect(mockDisconnect).toHaveBeenCalled()});it("recreates observer when scrollpoints change",()=>{const elem1=document.createElement("div");elem1.id="zone1";const elem2=document.createElement("div");elem2.id="zone2";document.body.appendChild(elem1);document.body.appendChild(elem2);const{rerender}=renderHook(({scrollpoints})=>useThemedScrollpoints(scrollpoints),{initialProps:{scrollpoints:[{id:"zone1",className:"theme-light"}]}});expect(IntersectionObserver).toHaveBeenCalledTimes(1);expect(mockObserve).toHaveBeenCalledTimes(1);rerender({scrollpoints:[{id:"zone1",className:"theme-light"},{id:"zone2",className:"theme-dark"}]});expect(mockDisconnect).toHaveBeenCalledTimes(1);expect(IntersectionObserver).toHaveBeenCalledTimes(2);expect(mockObserve).toHaveBeenCalledTimes(3)});it("uses requestAnimationFrame for state updates",()=>{const elem=document.createElement("div");elem.id="zone1";document.body.appendChild(elem);const scrollpoints=[{id:"zone1",className:"theme-light"},{id:"zone2",className:"theme-dark"}];renderHook(()=>useThemedScrollpoints(scrollpoints));const rafSpy=vi.spyOn(global,"requestAnimationFrame");observerCallback([{target:elem,isIntersecting:true}],{});expect(rafSpy).toHaveBeenCalled();rafSpy.mockRestore()})});
|
|
2
|
+
//# sourceMappingURL=use-themed-scrollpoints.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/core/hooks/use-themed-scrollpoints.test.ts"],"sourcesContent":["/**\n * @vitest-environment jsdom\n */\n\nimport { describe, it, expect, beforeEach, afterEach, vi } from \"vitest\";\nimport { renderHook, waitFor } from \"@testing-library/react\";\nimport { useThemedScrollpoints } from \"./use-themed-scrollpoints\";\n\ndescribe(\"useThemedScrollpoints\", () => {\n let observerCallback: IntersectionObserverCallback;\n let mockObserve: ReturnType<typeof vi.fn>;\n let mockUnobserve: ReturnType<typeof vi.fn>;\n let mockDisconnect: ReturnType<typeof vi.fn>;\n\n beforeEach(() => {\n // Mock IntersectionObserver\n mockObserve = vi.fn();\n mockUnobserve = vi.fn();\n mockDisconnect = vi.fn();\n\n global.IntersectionObserver = vi.fn((callback) => {\n observerCallback = callback;\n return {\n observe: mockObserve,\n unobserve: mockUnobserve,\n disconnect: mockDisconnect,\n };\n }) as any;\n\n // Mock requestAnimationFrame\n global.requestAnimationFrame = vi.fn((cb) => {\n cb(0);\n return 0;\n });\n });\n\n afterEach(() => {\n vi.clearAllMocks();\n document.body.innerHTML = \"\";\n });\n\n it(\"returns first scrollpoint className on mount\", () => {\n const scrollpoints = [\n { id: \"zone1\", className: \"theme-light\" },\n { id: \"zone2\", className: \"theme-dark\" },\n ];\n\n const { result } = renderHook(() => useThemedScrollpoints(scrollpoints));\n\n expect(result.current).toBe(\"theme-light\");\n });\n\n it(\"returns empty string when no scrollpoints provided\", () => {\n const { result } = renderHook(() => useThemedScrollpoints([]));\n expect(result.current).toBe(\"\");\n });\n\n it(\"does not create IntersectionObserver when no scrollpoints provided\", () => {\n renderHook(() => useThemedScrollpoints([]));\n\n expect(IntersectionObserver).not.toHaveBeenCalled();\n });\n\n it(\"creates IntersectionObserver with correct config\", () => {\n const scrollpoints = [{ id: \"zone1\", className: \"theme-light\" }];\n\n // Create DOM element\n const elem = document.createElement(\"div\");\n elem.id = \"zone1\";\n document.body.appendChild(elem);\n\n renderHook(() => useThemedScrollpoints(scrollpoints));\n\n expect(IntersectionObserver).toHaveBeenCalledWith(\n expect.any(Function),\n expect.objectContaining({\n rootMargin: \"-64px 0px 0px 0px\",\n threshold: 0,\n })\n );\n });\n\n it(\"observes all scrollpoint elements that exist in DOM\", () => {\n // Create DOM elements\n const elem1 = document.createElement(\"div\");\n elem1.id = \"zone1\";\n const elem2 = document.createElement(\"div\");\n elem2.id = \"zone2\";\n document.body.appendChild(elem1);\n document.body.appendChild(elem2);\n\n const scrollpoints = [\n { id: \"zone1\", className: \"theme-light\" },\n { id: \"zone2\", className: \"theme-dark\" },\n ];\n\n renderHook(() => useThemedScrollpoints(scrollpoints));\n\n expect(mockObserve).toHaveBeenCalledTimes(2);\n expect(mockObserve).toHaveBeenCalledWith(elem1);\n expect(mockObserve).toHaveBeenCalledWith(elem2);\n });\n\n it(\"logs warning for missing DOM elements\", () => {\n const consoleWarn = vi.spyOn(console, \"warn\").mockImplementation(() => {});\n\n const scrollpoints = [{ id: \"non-existent\", className: \"theme\" }];\n\n renderHook(() => useThemedScrollpoints(scrollpoints));\n\n expect(consoleWarn).toHaveBeenCalledWith(\n expect.stringContaining('Element with id \"non-existent\" not found')\n );\n\n consoleWarn.mockRestore();\n });\n\n it(\"observes existing elements and warns about missing ones\", () => {\n const consoleWarn = vi.spyOn(console, \"warn\").mockImplementation(() => {});\n\n // Create only one element\n const elem = document.createElement(\"div\");\n elem.id = \"zone1\";\n document.body.appendChild(elem);\n\n const scrollpoints = [\n { id: \"zone1\", className: \"theme-light\" },\n { id: \"missing\", className: \"theme-dark\" },\n ];\n\n renderHook(() => useThemedScrollpoints(scrollpoints));\n\n expect(mockObserve).toHaveBeenCalledTimes(1);\n expect(mockObserve).toHaveBeenCalledWith(elem);\n expect(consoleWarn).toHaveBeenCalledWith(\n expect.stringContaining('Element with id \"missing\" not found')\n );\n\n consoleWarn.mockRestore();\n });\n\n it(\"updates className when intersection occurs\", async () => {\n // Create DOM elements\n const elem1 = document.createElement(\"div\");\n elem1.id = \"zone1\";\n const elem2 = document.createElement(\"div\");\n elem2.id = \"zone2\";\n document.body.appendChild(elem1);\n document.body.appendChild(elem2);\n\n const scrollpoints = [\n { id: \"zone1\", className: \"theme-light\" },\n { id: \"zone2\", className: \"theme-dark\" },\n ];\n\n const { result } = renderHook(() => useThemedScrollpoints(scrollpoints));\n\n // Initial state\n expect(result.current).toBe(\"theme-light\");\n\n // Simulate intersection with zone2\n observerCallback(\n [\n {\n target: elem2,\n isIntersecting: true,\n } as unknown as IntersectionObserverEntry,\n ],\n {} as IntersectionObserver\n );\n\n await waitFor(() => {\n expect(result.current).toBe(\"theme-dark\");\n });\n });\n\n it(\"does not update className if same scrollpoint intersects again\", async () => {\n const elem = document.createElement(\"div\");\n elem.id = \"zone1\";\n document.body.appendChild(elem);\n\n const scrollpoints = [{ id: \"zone1\", className: \"theme-light\" }];\n\n const { result } = renderHook(() => useThemedScrollpoints(scrollpoints));\n\n expect(result.current).toBe(\"theme-light\");\n\n // Simulate intersection with same element\n const renderCount = result.current;\n observerCallback(\n [\n {\n target: elem,\n isIntersecting: true,\n } as unknown as IntersectionObserverEntry,\n ],\n {} as IntersectionObserver\n );\n\n // Should not trigger re-render (className unchanged)\n expect(result.current).toBe(renderCount);\n });\n\n it(\"ignores non-intersecting entries\", async () => {\n const elem1 = document.createElement(\"div\");\n elem1.id = \"zone1\";\n const elem2 = document.createElement(\"div\");\n elem2.id = \"zone2\";\n document.body.appendChild(elem1);\n document.body.appendChild(elem2);\n\n const scrollpoints = [\n { id: \"zone1\", className: \"theme-light\" },\n { id: \"zone2\", className: \"theme-dark\" },\n ];\n\n const { result } = renderHook(() => useThemedScrollpoints(scrollpoints));\n\n expect(result.current).toBe(\"theme-light\");\n\n // Simulate non-intersecting entry\n observerCallback(\n [\n {\n target: elem2,\n isIntersecting: false,\n } as unknown as IntersectionObserverEntry,\n ],\n {} as IntersectionObserver\n );\n\n // Should remain unchanged\n expect(result.current).toBe(\"theme-light\");\n });\n\n it(\"uses first intersecting entry when multiple entries intersect\", async () => {\n const elem1 = document.createElement(\"div\");\n elem1.id = \"zone1\";\n const elem2 = document.createElement(\"div\");\n elem2.id = \"zone2\";\n const elem3 = document.createElement(\"div\");\n elem3.id = \"zone3\";\n document.body.appendChild(elem1);\n document.body.appendChild(elem2);\n document.body.appendChild(elem3);\n\n const scrollpoints = [\n { id: \"zone1\", className: \"theme-light\" },\n { id: \"zone2\", className: \"theme-dark\" },\n { id: \"zone3\", className: \"theme-blue\" },\n ];\n\n const { result } = renderHook(() => useThemedScrollpoints(scrollpoints));\n\n // Simulate multiple intersections (zone2 and zone3 both intersecting)\n observerCallback(\n [\n {\n target: elem2,\n isIntersecting: true,\n } as unknown as IntersectionObserverEntry,\n {\n target: elem3,\n isIntersecting: true,\n } as unknown as IntersectionObserverEntry,\n ],\n {} as IntersectionObserver\n );\n\n await waitFor(() => {\n // Should use first intersecting entry (zone2)\n expect(result.current).toBe(\"theme-dark\");\n });\n });\n\n it(\"disconnects observer on unmount\", () => {\n const elem = document.createElement(\"div\");\n elem.id = \"zone1\";\n document.body.appendChild(elem);\n\n const scrollpoints = [{ id: \"zone1\", className: \"theme-light\" }];\n\n const { unmount } = renderHook(() => useThemedScrollpoints(scrollpoints));\n\n unmount();\n\n expect(mockDisconnect).toHaveBeenCalled();\n });\n\n it(\"recreates observer when scrollpoints change\", () => {\n const elem1 = document.createElement(\"div\");\n elem1.id = \"zone1\";\n const elem2 = document.createElement(\"div\");\n elem2.id = \"zone2\";\n document.body.appendChild(elem1);\n document.body.appendChild(elem2);\n\n const { rerender } = renderHook(\n ({ scrollpoints }) => useThemedScrollpoints(scrollpoints),\n {\n initialProps: {\n scrollpoints: [{ id: \"zone1\", className: \"theme-light\" }],\n },\n }\n );\n\n expect(IntersectionObserver).toHaveBeenCalledTimes(1);\n expect(mockObserve).toHaveBeenCalledTimes(1);\n\n // Change scrollpoints\n rerender({\n scrollpoints: [\n { id: \"zone1\", className: \"theme-light\" },\n { id: \"zone2\", className: \"theme-dark\" },\n ],\n });\n\n // Should disconnect old observer and create new one\n expect(mockDisconnect).toHaveBeenCalledTimes(1);\n expect(IntersectionObserver).toHaveBeenCalledTimes(2);\n expect(mockObserve).toHaveBeenCalledTimes(3); // 1 from first render + 2 from second\n });\n\n it(\"uses requestAnimationFrame for state updates\", () => {\n const elem = document.createElement(\"div\");\n elem.id = \"zone1\";\n document.body.appendChild(elem);\n\n const scrollpoints = [\n { id: \"zone1\", className: \"theme-light\" },\n { id: \"zone2\", className: \"theme-dark\" },\n ];\n\n renderHook(() => useThemedScrollpoints(scrollpoints));\n\n const rafSpy = vi.spyOn(global, \"requestAnimationFrame\");\n\n // Simulate intersection\n observerCallback(\n [\n {\n target: elem,\n isIntersecting: true,\n } as unknown as IntersectionObserverEntry,\n ],\n {} as IntersectionObserver\n );\n\n expect(rafSpy).toHaveBeenCalled();\n\n rafSpy.mockRestore();\n });\n});\n"],"names":["describe","it","expect","beforeEach","afterEach","vi","renderHook","waitFor","useThemedScrollpoints","observerCallback","mockObserve","mockUnobserve","mockDisconnect","fn","global","IntersectionObserver","callback","observe","unobserve","disconnect","requestAnimationFrame","cb","clearAllMocks","document","body","innerHTML","scrollpoints","id","className","result","current","toBe","not","toHaveBeenCalled","elem","createElement","appendChild","toHaveBeenCalledWith","any","Function","objectContaining","rootMargin","threshold","elem1","elem2","toHaveBeenCalledTimes","consoleWarn","spyOn","console","mockImplementation","stringContaining","mockRestore","target","isIntersecting","renderCount","elem3","unmount","rerender","initialProps","rafSpy"],"mappings":"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"}
|
package/index.d.ts
CHANGED
|
@@ -773,12 +773,18 @@ export const HeaderLinks: React.FC<Pick<HeaderProps, "sessionState" | "headerLin
|
|
|
773
773
|
//# sourceMappingURL=HeaderLinks.d.ts.map
|
|
774
774
|
}
|
|
775
775
|
|
|
776
|
-
declare module '@ably/ui/core/Header' {
|
|
777
|
-
import React, { ReactNode } from "react";
|
|
776
|
+
declare module '@ably/ui/core/Header/types' {
|
|
778
777
|
export type ThemedScrollpoint = {
|
|
779
778
|
id: string;
|
|
780
779
|
className: string;
|
|
781
780
|
};
|
|
781
|
+
//# sourceMappingURL=types.d.ts.map
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
declare module '@ably/ui/core/Header' {
|
|
785
|
+
import React, { ReactNode } from "react";
|
|
786
|
+
import { ThemedScrollpoint } from "@ably/ui/core/Header/types";
|
|
787
|
+
export type { ThemedScrollpoint };
|
|
782
788
|
/**
|
|
783
789
|
* Represents the state of the user session in the header.
|
|
784
790
|
*/
|
|
@@ -5999,6 +6005,20 @@ export default useRailsUjsLinks;
|
|
|
5999
6005
|
//# sourceMappingURL=use-rails-ujs-hooks.d.ts.map
|
|
6000
6006
|
}
|
|
6001
6007
|
|
|
6008
|
+
declare module '@ably/ui/core/hooks/use-themed-scrollpoints' {
|
|
6009
|
+
import { ThemedScrollpoint } from ".@ably/ui/core/Header/types";
|
|
6010
|
+
export function useThemedScrollpoints(scrollpoints: ThemedScrollpoint[]): string;
|
|
6011
|
+
//# sourceMappingURL=use-themed-scrollpoints.d.ts.map
|
|
6012
|
+
}
|
|
6013
|
+
|
|
6014
|
+
declare module '@ably/ui/core/hooks/use-themed-scrollpoints.test' {
|
|
6015
|
+
/**
|
|
6016
|
+
* @vitest-environment jsdom
|
|
6017
|
+
*/
|
|
6018
|
+
export {};
|
|
6019
|
+
//# sourceMappingURL=use-themed-scrollpoints.test.d.ts.map
|
|
6020
|
+
}
|
|
6021
|
+
|
|
6002
6022
|
declare module '@ably/ui/core/insights/command-queue' {
|
|
6003
6023
|
import { AnalyticsService, InsightsConfig, InsightsIdentity, TrackPageViewOptions } from "@ably/ui/core/types";
|
|
6004
6024
|
export class InsightsCommandQueue implements AnalyticsService {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ably/ui",
|
|
3
|
-
"version": "17.
|
|
3
|
+
"version": "17.11.0-dev.4c4b6b55",
|
|
4
4
|
"description": "Home of the Ably design system library ([design.ably.com](https://design.ably.com)). It provides a showcase, development/test environment and a publishing pipeline for different distributables.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
"reset",
|
|
13
13
|
"tailwind.config.js",
|
|
14
14
|
"tailwind.extend.js",
|
|
15
|
-
"index.d.ts"
|
|
15
|
+
"index.d.ts",
|
|
16
|
+
"AGENTS.md",
|
|
17
|
+
"CLAUDE.md"
|
|
16
18
|
],
|
|
17
19
|
"types": "index.d.ts",
|
|
18
20
|
"msw": {
|
|
@@ -29,6 +31,7 @@
|
|
|
29
31
|
"@swc/cli": "^0.7.8",
|
|
30
32
|
"@swc/core": "^1.13.5",
|
|
31
33
|
"@tailwindcss/container-queries": "^0.1.1",
|
|
34
|
+
"@testing-library/react": "^16.3.0",
|
|
32
35
|
"@types/js-cookie": "^3.0.6",
|
|
33
36
|
"@types/node": "^20",
|
|
34
37
|
"@types/react": "^18.3.1",
|