@axzydev/axzy_ui_system 1.3.1 → 1.3.3
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/README.md +6 -2
- package/dist/index.cjs +957 -554
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.cts +191 -9
- package/dist/index.d.ts +191 -9
- package/dist/index.js +908 -505
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/App.tsx +150 -154
- package/src/components/molecules/table/table.doc.mdx +64 -21
- package/src/components/molecules/table/table.props.ts +81 -2
- package/src/components/molecules/table/table.stories.tsx +155 -0
- package/src/components/molecules/table/table.tsx +178 -36
- package/src/components/organisms/data-table/ITDataTable.doc.mdx +37 -3
- package/src/components/organisms/data-table/ITDataTable.stories.tsx +43 -0
- package/src/components/organisms/data-table/dataTable.props.ts +64 -0
- package/src/components/organisms/data-table/dataTable.tsx +241 -82
- package/src/components/organisms/sidebar/sidebar.doc.mdx +5 -0
- package/src/components/organisms/sidebar/sidebar.props.ts +5 -0
- package/src/components/organisms/sidebar/sidebar.stories.tsx +1 -1
- package/src/components/organisms/sidebar/sidebar.tsx +27 -7
- package/src/components/organisms/topbar/topbar.doc.mdx +3 -0
- package/src/components/organisms/topbar/topbar.props.ts +16 -0
- package/src/components/organisms/topbar/topbar.tsx +133 -98
- package/src/components/templates/layout/layout.doc.mdx +2 -0
- package/src/components/templates/layout/layout.tsx +7 -1
- package/src/components/templates/page/page.doc.mdx +8 -2
- package/src/components/templates/page/page.props.ts +11 -1
- package/src/components/templates/page/page.tsx +11 -1
- package/src/components/theme-provider/themePresets.ts +6 -0
- package/src/components/theme-provider/themeProvider.doc.mdx +1 -0
- package/src/components/theme-provider/themeProvider.props.ts +2 -0
- package/src/components/theme-provider/themeProvider.tsx +13 -2
- package/src/dev.css +58 -1
- package/src/hooks/useElementSize.ts +46 -0
- package/src/hooks/useVirtualRows.ts +123 -0
- package/src/index.ts +2 -0
- package/src/sandbox/LandingShowcase.tsx +654 -0
- package/src/sandbox/NotFoundShowcase.tsx +70 -0
- package/src/sandbox/TopbarSearch.tsx +85 -0
- package/src/sandbox/navigation.ts +148 -0
- package/src/sandbox/useHashRoute.ts +149 -0
- package/src/showcases/DataShowcases.tsx +321 -24
- package/src/showcases/PageShowcases.tsx +31 -0
- package/src/showcases/StructureShowcases.tsx +42 -4
- package/src/theme/theme.types.ts +2 -0
- package/src/types/table.types.ts +48 -0
- package/src/utils/styles.ts +12 -5
- package/src/utils/table.utils.ts +31 -0
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
1
3
|
export interface ITTopBarNavItem {
|
|
2
4
|
/** Unique identifier for the navigation item. */
|
|
3
5
|
id: string;
|
|
@@ -14,6 +16,20 @@ export interface ITTopBarProps {
|
|
|
14
16
|
logo?: any;
|
|
15
17
|
/** Text displayed next to the logo. */
|
|
16
18
|
logoText?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Content rendered in the centered region between the left (logo / nav) and
|
|
21
|
+
* right (user menu) areas — e.g. a global search box. The region is rendered
|
|
22
|
+
* only when this prop is provided, and only from the `lg` breakpoint up;
|
|
23
|
+
* below `lg` the row stays logo + user menu so nothing is squeezed.
|
|
24
|
+
*/
|
|
25
|
+
centerContent?: ReactNode;
|
|
26
|
+
/**
|
|
27
|
+
* Actions rendered in the right area, immediately before the user menu —
|
|
28
|
+
* e.g. a notification bell, a help link, a theme switcher. The component
|
|
29
|
+
* decides the placement, so the caller only supplies the controls. The area
|
|
30
|
+
* is rendered only when this prop or `userMenu` is provided.
|
|
31
|
+
*/
|
|
32
|
+
children?: ReactNode;
|
|
17
33
|
/** User dropdown configuration including name, email, avatar, and menu items. */
|
|
18
34
|
userMenu?: {
|
|
19
35
|
/** Display name shown in the trigger button and dropdown header. */
|
|
@@ -41,11 +41,28 @@ import ITText from "@/components/atoms/text/text";
|
|
|
41
41
|
* showMobileMenuButton
|
|
42
42
|
* onToggleMobileMenu={() => setSidebarOpen((v) => !v)}
|
|
43
43
|
* />
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* // Topbar with a global search box in the centered region (lg and up)
|
|
47
|
+
* <ITTopBar
|
|
48
|
+
* logoText="AXZY"
|
|
49
|
+
* centerContent={<SearchBox value={term} onChange={setTerm} />}
|
|
50
|
+
* userMenu={{ userName: "Jane Doe", userEmail: "jane@example.com", menuItems: [] }}
|
|
51
|
+
* />
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* // Topbar with a notification bell passed as children, sitting right before
|
|
55
|
+
* // the user menu. The slot is placed by the component, not by the caller.
|
|
56
|
+
* <ITTopBar logoText="AXZY" userMenu={userMenu}>
|
|
57
|
+
* <NotificationBell count={3} />
|
|
58
|
+
* </ITTopBar>
|
|
44
59
|
*/
|
|
45
60
|
export default function ITTopBar({
|
|
46
61
|
logo,
|
|
47
62
|
logoText,
|
|
63
|
+
centerContent,
|
|
48
64
|
userMenu,
|
|
65
|
+
children,
|
|
49
66
|
showMobileMenuButton,
|
|
50
67
|
onToggleMobileMenu,
|
|
51
68
|
navItems,
|
|
@@ -131,110 +148,128 @@ export default function ITTopBar({
|
|
|
131
148
|
)}
|
|
132
149
|
</div>
|
|
133
150
|
|
|
134
|
-
{/*
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
151
|
+
{/* CENTER AREA: optional slot (e.g. global search) — lg and up only.
|
|
152
|
+
`flex-1` is basis 0, so it absorbs every pixel of slack and
|
|
153
|
+
`justify-between` has nothing left to distribute: the logo/nav and
|
|
154
|
+
the user menu stay flush with the row edges. */}
|
|
155
|
+
{centerContent && (
|
|
156
|
+
<div className="hidden lg:flex flex-1 min-w-0 items-center justify-center px-4">
|
|
157
|
+
<div className="w-full max-w-[420px]">{centerContent}</div>
|
|
158
|
+
</div>
|
|
159
|
+
)}
|
|
160
|
+
|
|
161
|
+
{/* RIGHT AREA: optional children (e.g. a notification bell) + the user menu.
|
|
162
|
+
Both share one flex group so `justify-between` on the row keeps the pair
|
|
163
|
+
flush to the right edge instead of pushing the bell into the middle of
|
|
164
|
+
the bar. */}
|
|
165
|
+
{(children || userMenu) && (
|
|
166
|
+
<div className="flex items-center gap-2">
|
|
167
|
+
{children}
|
|
168
|
+
{userMenu && (
|
|
152
169
|
<div className="relative">
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
170
|
+
<button
|
|
171
|
+
type="button"
|
|
172
|
+
className="flex items-center gap-3 rounded-full pl-2 pr-4 py-1.5 transition-all duration-200 ease-[cubic-bezier(0.2,0,0,1)] border border-transparent hover:border-secondary-200"
|
|
173
|
+
style={{
|
|
174
|
+
backgroundColor: isUserMenuOpen ? "var(--it-topbar-user-hover, #f1f5f9)" : "transparent",
|
|
175
|
+
}}
|
|
176
|
+
onMouseEnter={(e) => {
|
|
177
|
+
if (!isUserMenuOpen) e.currentTarget.style.backgroundColor = "var(--it-topbar-user-hover, #f1f5f9)";
|
|
178
|
+
}}
|
|
179
|
+
onMouseLeave={(e) => {
|
|
180
|
+
if (!isUserMenuOpen) e.currentTarget.style.backgroundColor = "transparent";
|
|
181
|
+
}}
|
|
182
|
+
onClick={() => setIsUserMenuOpen(!isUserMenuOpen)}
|
|
183
|
+
>
|
|
184
|
+
{/* Avatar */}
|
|
185
|
+
<div className="relative">
|
|
186
|
+
{userMenu.userImage ? (
|
|
187
|
+
<img
|
|
188
|
+
className="w-9 h-9 rounded-full object-cover ring-2 ring-white shadow-sm"
|
|
189
|
+
src={userMenu.userImage}
|
|
190
|
+
alt="Current user"
|
|
191
|
+
/>
|
|
192
|
+
) : (
|
|
193
|
+
<div className="w-9 h-9 rounded-full bg-slate-100 flex items-center justify-center ring-2 ring-white shadow-sm">
|
|
194
|
+
<FaUserCircle className="w-6 h-6" style={{ color: "var(--it-topbar-icon, #94a3b8)" }} />
|
|
195
|
+
</div>
|
|
196
|
+
)}
|
|
197
|
+
{/* Active dot indicator */}
|
|
198
|
+
<div className="absolute bottom-0 right-0 w-2.5 h-2.5 bg-green-500 border-2 border-white rounded-full"></div>
|
|
162
199
|
</div>
|
|
163
|
-
)}
|
|
164
|
-
{/* Active dot indicator */}
|
|
165
|
-
<div className="absolute bottom-0 right-0 w-2.5 h-2.5 bg-green-500 border-2 border-white rounded-full"></div>
|
|
166
|
-
</div>
|
|
167
200
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
201
|
+
{/* Name Details */}
|
|
202
|
+
<div className="hidden sm:flex flex-col text-left py-0.5">
|
|
203
|
+
<ITText as="span"
|
|
204
|
+
className="font-semibold text-[0.85rem] leading-tight"
|
|
205
|
+
style={{ color: "var(--it-topbar-user-text, #0f172a)" }}
|
|
206
|
+
>
|
|
207
|
+
{userMenu.userName}
|
|
208
|
+
</ITText>
|
|
209
|
+
<ITText as="span"
|
|
210
|
+
className="text-[0.7rem] font-medium"
|
|
211
|
+
style={{ color: "var(--it-topbar-user-subtitle, #64748b)" }}
|
|
212
|
+
>
|
|
213
|
+
{userMenu.userEmail}
|
|
214
|
+
</ITText>
|
|
215
|
+
</div>
|
|
216
|
+
</button>
|
|
184
217
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
218
|
+
{/* Dropdown Menu */}
|
|
219
|
+
<div
|
|
220
|
+
ref={userMenuRef}
|
|
221
|
+
className={`
|
|
222
|
+
absolute right-0 mt-3 w-64 rounded-2xl shadow-[0_10px_40px_-10px_rgba(0,0,0,0.15)] z-[70] overflow-hidden transform origin-top-right transition-all duration-200 ease-[cubic-bezier(0.2,0,0,1)]
|
|
223
|
+
${isUserMenuOpen ? "scale-100 opacity-100 translate-y-0" : "scale-95 opacity-0 -translate-y-2 pointer-events-none"}
|
|
224
|
+
`}
|
|
225
|
+
style={{
|
|
226
|
+
backgroundColor: "var(--it-topbar-user-dropdown-bg, #ffffff)",
|
|
227
|
+
border: "1px solid var(--it-topbar-user-dropdown-border, #f1f5f9)"
|
|
228
|
+
}}
|
|
229
|
+
>
|
|
230
|
+
{/* Dropdown Header */}
|
|
231
|
+
<div className="px-5 py-4 border-b" style={{ borderColor: "var(--it-topbar-user-dropdown-border, #f1f5f9)", backgroundColor: "var(--it-topbar-user-bg, #f8fafc)" }}>
|
|
232
|
+
<ITText as="span" className="block font-bold text-[0.9rem]" style={{ color: "var(--it-topbar-user-text, #0f172a)" }}>
|
|
233
|
+
{userMenu.userName}
|
|
234
|
+
</ITText>
|
|
235
|
+
<ITText as="span" className="block text-xs font-medium truncate mt-0.5" style={{ color: "var(--it-topbar-user-subtitle, #64748b)" }}>
|
|
236
|
+
{userMenu.userEmail}
|
|
237
|
+
</ITText>
|
|
238
|
+
</div>
|
|
206
239
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
240
|
+
{/* Dropdown Items */}
|
|
241
|
+
<ul className="py-2">
|
|
242
|
+
{userMenu.menuItems.map((m, i) => {
|
|
243
|
+
const isDestructive = m.label.toLowerCase().includes('salir') || m.label.toLowerCase().includes('cerrar') || m.label.toLowerCase().includes('logout');
|
|
211
244
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
245
|
+
return (
|
|
246
|
+
<li key={i} className="px-2">
|
|
247
|
+
{i === userMenu.menuItems.length - 1 && isDestructive && i > 0 && (
|
|
248
|
+
<div className="h-px bg-slate-100 my-1 mx-2"></div>
|
|
249
|
+
)}
|
|
250
|
+
<button
|
|
251
|
+
onClick={() => {
|
|
252
|
+
m.onClick();
|
|
253
|
+
setIsUserMenuOpen(false);
|
|
254
|
+
}}
|
|
255
|
+
className="block w-full text-left px-3 py-2.5 rounded-xl text-[0.875rem] font-medium transition-colors duration-150"
|
|
256
|
+
style={{ color: isDestructive ? 'var(--color-danger-500)' : "var(--it-topbar-user-text, #334155)" }}
|
|
257
|
+
onMouseEnter={(e) => {
|
|
258
|
+
e.currentTarget.style.backgroundColor = isDestructive ? 'var(--color-danger-50)' : "var(--it-topbar-user-item-hover, #f8fafc)";
|
|
259
|
+
}}
|
|
260
|
+
onMouseLeave={(e) => {
|
|
261
|
+
e.currentTarget.style.backgroundColor = "transparent";
|
|
262
|
+
}}
|
|
263
|
+
>
|
|
264
|
+
<ITText as="span">{m.label}</ITText>
|
|
265
|
+
</button>
|
|
266
|
+
</li>
|
|
267
|
+
)
|
|
268
|
+
})}
|
|
269
|
+
</ul>
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
)}
|
|
238
273
|
</div>
|
|
239
274
|
)}
|
|
240
275
|
</div>
|
|
@@ -40,7 +40,9 @@ import { ITLayout } from '@axzydev/axzy_ui_system';
|
|
|
40
40
|
|
|
41
41
|
- The topbar spans the full viewport width at the top of the page.
|
|
42
42
|
- The sidebar sits under the topbar on the left and floats over content when expanded via hover.
|
|
43
|
+
- Reserved desktop width follows `sidebar.isCollapsed`: omitted/uncontrolled or `true` reserves the 88px rail (hover expands over content); controlled-expanded (`sidebar.isCollapsed={false}`) reserves the full 280px so it never overlays content.
|
|
43
44
|
- Sidebar in collapsed state shows only icons; expanding reveals labels.
|
|
45
|
+
- The collapsed rail expands automatically on hover. Keep global search in `topBar.centerContent` (always visible, `lg` and up) rather than `sidebar.header`, which is expanded-only and disappears in the 88px rail.
|
|
44
46
|
- Themed entirely through CSS custom properties from ITThemeProvider.
|
|
45
47
|
- Includes mobile responsive behavior with a hamburger-style toggle.
|
|
46
48
|
|
|
@@ -58,7 +58,13 @@ export default function ITLayout({
|
|
|
58
58
|
>
|
|
59
59
|
{/* DESKTOP SIDEBAR */}
|
|
60
60
|
<div className="hidden lg:block relative z-40 h-full">
|
|
61
|
-
|
|
61
|
+
{/* Reserves the sidebar's at-rest width. When the sidebar is collapsed
|
|
62
|
+
(default), this is the 88px rail and hover expands over the content.
|
|
63
|
+
When controlled-expanded (`sidebar.isCollapsed === false`), it
|
|
64
|
+
reserves the full 280px so the sidebar never overlays content. */}
|
|
65
|
+
<div
|
|
66
|
+
className={`${desktopCollapsed ? "w-[88px]" : "w-[280px]"} h-full flex-shrink-0`}
|
|
67
|
+
/>
|
|
62
68
|
<div className="absolute top-0 left-0 h-full">
|
|
63
69
|
<ITSidebar
|
|
64
70
|
{...sidebar}
|
|
@@ -29,7 +29,8 @@ A page template component that wraps an ITPageHeader with content area and built
|
|
|
29
29
|
| `emptyAction` | `ReactNode` | `undefined` | Custom action element for the empty state. |
|
|
30
30
|
| `children` | `ReactNode` | required | Content rendered when no special state (loading/error/empty) is active. |
|
|
31
31
|
| `maxWidth` | `'2xl' \| '3xl' \| '4xl' \| '5xl' \| '6xl' \| '7xl'` | `'7xl'` | Maximum width of the page content container. |
|
|
32
|
-
| `noPadding` | `boolean` | `false` | When true, removes default padding from the page wrapper. |
|
|
32
|
+
| `noPadding` | `boolean` | `false` | When true, removes default padding from the page wrapper. Overrides `horizontalPadding`. |
|
|
33
|
+
| `horizontalPadding` | `string` | `'px-4 sm:px-6 lg:px-8'` | Tailwind classes for the wrapper's horizontal padding. Use `'px-0'` for edge-to-edge content. Must be a literal — Tailwind scans source, so runtime-assembled values are not generated. |
|
|
33
34
|
| `className` | `string` | `undefined` | Additional CSS classes for the page wrapper. |
|
|
34
35
|
|
|
35
36
|
## Usage
|
|
@@ -71,7 +72,12 @@ import { ITPage } from '@axzydev/axzy_ui_system';
|
|
|
71
72
|
- The empty state renders a centered placeholder with optional title, description, and action.
|
|
72
73
|
- All header props (`title`, `description`, `breadcrumbs`, `actions`, `backAction`, `icon`) are forwarded to ITPageHeader.
|
|
73
74
|
- Content maximum width is controlled by Tailwind max-width classes via `maxWidth`.
|
|
74
|
-
- `noPadding` is useful for full-bleed layouts like dashboards.
|
|
75
|
+
- `noPadding` is useful for full-bleed layouts like dashboards. It removes both axes and wins over `horizontalPadding`.
|
|
76
|
+
- `horizontalPadding` tunes only the horizontal gutter, so you can keep the vertical rhythm while going edge-to-edge:
|
|
77
|
+
```tsx
|
|
78
|
+
<ITPage title="Reportes" horizontalPadding="px-0">...</ITPage>
|
|
79
|
+
```
|
|
80
|
+
Because Tailwind scans your source files, write the value as a literal. A value built at runtime (`"px-" + n`) will silently produce no padding.
|
|
75
81
|
|
|
76
82
|
## Stories
|
|
77
83
|
|
|
@@ -41,6 +41,16 @@ export interface ITPageProps {
|
|
|
41
41
|
children: ReactNode;
|
|
42
42
|
/** Maximum width of the page content. Options: "2xl", "3xl", "4xl", "5xl", "6xl", "7xl". Default: "7xl". */
|
|
43
43
|
maxWidth?: "2xl" | "3xl" | "4xl" | "5xl" | "6xl" | "7xl";
|
|
44
|
-
/** Whether to remove default padding from the page wrapper. */
|
|
44
|
+
/** Whether to remove default padding from the page wrapper. Overrides `horizontalPadding`. */
|
|
45
45
|
noPadding?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Tailwind classes for the page wrapper's horizontal padding. Pass `"px-0"`
|
|
48
|
+
* for edge-to-edge content, or a responsive scale like `"px-2 sm:px-4"`.
|
|
49
|
+
*
|
|
50
|
+
* Must be written as a literal: Tailwind scans source files, so a value
|
|
51
|
+
* assembled at runtime will not be generated into the stylesheet.
|
|
52
|
+
*
|
|
53
|
+
* @default "px-4 sm:px-6 lg:px-8"
|
|
54
|
+
*/
|
|
55
|
+
horizontalPadding?: string;
|
|
46
56
|
}
|
|
@@ -41,6 +41,14 @@ const renderHeader = (props: ITPageProps) => {
|
|
|
41
41
|
* <ITCard>Dashboard content</ITCard>
|
|
42
42
|
* </ITPage>
|
|
43
43
|
* ```
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```tsx
|
|
47
|
+
* // Edge-to-edge content: no horizontal gutter, vertical padding kept.
|
|
48
|
+
* <ITPage title="Reportes" horizontalPadding="px-0">
|
|
49
|
+
* <ITTable data={rows} columns={columns} />
|
|
50
|
+
* </ITPage>
|
|
51
|
+
* ```
|
|
44
52
|
*/
|
|
45
53
|
export default function ITPage(props: ITPageProps) {
|
|
46
54
|
const {
|
|
@@ -57,11 +65,13 @@ export default function ITPage(props: ITPageProps) {
|
|
|
57
65
|
children,
|
|
58
66
|
maxWidth = "7xl",
|
|
59
67
|
noPadding = false,
|
|
68
|
+
horizontalPadding = "px-4 sm:px-6 lg:px-8",
|
|
60
69
|
} = props;
|
|
61
70
|
|
|
62
71
|
const wrapperClass = clsx(
|
|
63
72
|
"mx-auto w-full",
|
|
64
|
-
!noPadding &&
|
|
73
|
+
!noPadding && horizontalPadding,
|
|
74
|
+
!noPadding && "py-6",
|
|
65
75
|
`max-w-${maxWidth}`,
|
|
66
76
|
"space-y-8",
|
|
67
77
|
className
|
|
@@ -21,6 +21,7 @@ export const DEFAULT_PALETTE: ITThemePalette = {
|
|
|
21
21
|
headerText: "#334155", // Slate-700
|
|
22
22
|
rowBg: "#ffffff", // White
|
|
23
23
|
rowText: "#1e293b", // Slate-800
|
|
24
|
+
rowHover: "#f1f5f9", // Slate-100
|
|
24
25
|
},
|
|
25
26
|
};
|
|
26
27
|
|
|
@@ -48,6 +49,7 @@ export const PRESETS: { name: string; colors: ITThemePalette }[] = [
|
|
|
48
49
|
headerText: "#334155",
|
|
49
50
|
rowBg: "#ffffff",
|
|
50
51
|
rowText: "#0f172a",
|
|
52
|
+
rowHover: "#1e293b",
|
|
51
53
|
},
|
|
52
54
|
},
|
|
53
55
|
},
|
|
@@ -73,6 +75,7 @@ export const PRESETS: { name: string; colors: ITThemePalette }[] = [
|
|
|
73
75
|
headerText: "#881337",
|
|
74
76
|
rowBg: "#ffffff",
|
|
75
77
|
rowText: "#1c1212",
|
|
78
|
+
rowHover: "#fff1f2",
|
|
76
79
|
},
|
|
77
80
|
},
|
|
78
81
|
},
|
|
@@ -98,6 +101,7 @@ export const PRESETS: { name: string; colors: ITThemePalette }[] = [
|
|
|
98
101
|
headerText: "#115e59",
|
|
99
102
|
rowBg: "#ffffff",
|
|
100
103
|
rowText: "#042f2e",
|
|
104
|
+
rowHover: "#f0fdfa",
|
|
101
105
|
},
|
|
102
106
|
},
|
|
103
107
|
},
|
|
@@ -123,6 +127,7 @@ export const PRESETS: { name: string; colors: ITThemePalette }[] = [
|
|
|
123
127
|
headerText: "#713f12",
|
|
124
128
|
rowBg: "#ffffff",
|
|
125
129
|
rowText: "#292524",
|
|
130
|
+
rowHover: "#fefce8",
|
|
126
131
|
},
|
|
127
132
|
},
|
|
128
133
|
},
|
|
@@ -148,6 +153,7 @@ export const PRESETS: { name: string; colors: ITThemePalette }[] = [
|
|
|
148
153
|
headerText: "#166534",
|
|
149
154
|
rowBg: "#ffffff",
|
|
150
155
|
rowText: "#052e16",
|
|
156
|
+
rowHover: "#f0fdf4",
|
|
151
157
|
},
|
|
152
158
|
},
|
|
153
159
|
},
|
|
@@ -38,6 +38,7 @@ Root theme context that injects CSS custom properties into the entire component
|
|
|
38
38
|
| `table.headerText` | `string` | Table header text color. |
|
|
39
39
|
| `table.rowBg` | `string` | Table row background color. |
|
|
40
40
|
| `table.rowText` | `string` | Table row text color. |
|
|
41
|
+
| `table.rowHover` | `string` | Table row background color while hovering a row. |
|
|
41
42
|
|
|
42
43
|
## Usage
|
|
43
44
|
|
|
@@ -36,6 +36,8 @@ export interface ITThemePalette {
|
|
|
36
36
|
rowBg: string;
|
|
37
37
|
/** Text color of table body rows. @default "#1e293b" */
|
|
38
38
|
rowText: string;
|
|
39
|
+
/** Background color applied to a table body row while the pointer is over it (hex). @default "#f1f5f9" */
|
|
40
|
+
rowHover: string;
|
|
39
41
|
};
|
|
40
42
|
}
|
|
41
43
|
|
|
@@ -364,6 +364,7 @@ export default function ITThemeProvider({
|
|
|
364
364
|
let tableRowText = palette.table.rowText;
|
|
365
365
|
let tableHeaderBg = palette.table.headerBg;
|
|
366
366
|
let tableHeaderText = palette.table.headerText;
|
|
367
|
+
let tableRowHover = palette.table.rowHover;
|
|
367
368
|
let navbarBg = palette.layout.navbarBg;
|
|
368
369
|
let navbarText = palette.layout.navbarText;
|
|
369
370
|
let sidebarBg = palette.layout.sidebarBg;
|
|
@@ -382,6 +383,9 @@ export default function ITThemeProvider({
|
|
|
382
383
|
if (!isLightColor(tableHeaderText)) {
|
|
383
384
|
tableHeaderText = `color-mix(in srgb, ${palette.table.headerText} 20%, #f9fafb)`;
|
|
384
385
|
}
|
|
386
|
+
if (isLightColor(tableRowHover)) {
|
|
387
|
+
tableRowHover = `color-mix(in srgb, ${palette.table.rowHover} 12%, #1f2937)`;
|
|
388
|
+
}
|
|
385
389
|
if (isLightColor(navbarBg)) {
|
|
386
390
|
navbarBg = `color-mix(in srgb, ${palette.layout.navbarBg} 8%, #111827)`;
|
|
387
391
|
}
|
|
@@ -419,10 +423,15 @@ export default function ITThemeProvider({
|
|
|
419
423
|
tableHeaderText = `color-mix(in srgb, ${palette.table.headerText} 30%, #334155)`;
|
|
420
424
|
} else if (
|
|
421
425
|
!isLightColor(tableHeaderText) &&
|
|
422
|
-
isVeryDarkColor(
|
|
426
|
+
isVeryDarkColor(tableHeaderText)
|
|
423
427
|
) {
|
|
424
428
|
// Dark header text is fine for light mode
|
|
425
429
|
}
|
|
430
|
+
if (!isLightColor(tableRowHover)) {
|
|
431
|
+
tableRowHover = isVeryDarkColor(palette.table.rowHover)
|
|
432
|
+
? "#f1f5f9"
|
|
433
|
+
: `color-mix(in srgb, ${palette.table.rowHover} 12%, #f8fafc)`;
|
|
434
|
+
}
|
|
426
435
|
if (!isLightColor(navbarBg)) {
|
|
427
436
|
navbarBg = isVeryDarkColor(palette.layout.navbarBg)
|
|
428
437
|
? "#ffffff"
|
|
@@ -637,6 +646,7 @@ export default function ITThemeProvider({
|
|
|
637
646
|
--color-table-headerText: ${tableHeaderText} !important;
|
|
638
647
|
--color-table-rowBg: ${tableRowBg} !important;
|
|
639
648
|
--color-table-rowText: ${tableRowText} !important;
|
|
649
|
+
--color-table-rowHover: ${tableRowHover} !important;
|
|
640
650
|
|
|
641
651
|
/* Native library variables integration */
|
|
642
652
|
--it-sidebar-bg: var(--color-sidebarBg);
|
|
@@ -980,7 +990,7 @@ export default function ITThemeProvider({
|
|
|
980
990
|
color: var(--color-table-rowText) !important;
|
|
981
991
|
}
|
|
982
992
|
tbody tr:hover {
|
|
983
|
-
background-color:
|
|
993
|
+
background-color: var(--color-table-rowHover) !important;
|
|
984
994
|
}
|
|
985
995
|
|
|
986
996
|
/* Animation for Saved Indicator */
|
|
@@ -1781,6 +1791,7 @@ export default function ITThemeProvider({
|
|
|
1781
1791
|
"table.headerText",
|
|
1782
1792
|
"table.rowBg",
|
|
1783
1793
|
"table.rowText",
|
|
1794
|
+
"table.rowHover",
|
|
1784
1795
|
].map((key) => (
|
|
1785
1796
|
<ColorRow key={key} colorKey={key} />
|
|
1786
1797
|
))}
|
package/src/dev.css
CHANGED
|
@@ -1,2 +1,59 @@
|
|
|
1
1
|
@import "tailwindcss";
|
|
2
|
-
@import "./index.css";
|
|
2
|
+
@import "./index.css";
|
|
3
|
+
|
|
4
|
+
/* ──────────────────────────────────────────────
|
|
5
|
+
SANDBOX-ONLY ANIMATIONS (dev.css)
|
|
6
|
+
Never shipped: only src/main.tsx imports this file and `pnpm bundle`
|
|
7
|
+
builds from src/index.ts, so these never reach dist/index.css.
|
|
8
|
+
────────────────────────────────────────────── */
|
|
9
|
+
|
|
10
|
+
@keyframes it-landing-rise {
|
|
11
|
+
from {
|
|
12
|
+
opacity: 0;
|
|
13
|
+
transform: translateY(14px);
|
|
14
|
+
}
|
|
15
|
+
to {
|
|
16
|
+
opacity: 1;
|
|
17
|
+
transform: translateY(0);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.it-landing-rise {
|
|
22
|
+
animation: it-landing-rise 0.6s cubic-bezier(0.2, 0.8, 0.2, 1) both;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@keyframes it-landing-float {
|
|
26
|
+
0%,
|
|
27
|
+
100% {
|
|
28
|
+
transform: translateY(0);
|
|
29
|
+
}
|
|
30
|
+
50% {
|
|
31
|
+
transform: translateY(-18px);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.it-landing-float {
|
|
36
|
+
animation: it-landing-float 8s ease-in-out infinite;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.it-landing-grid {
|
|
40
|
+
background-image: radial-gradient(
|
|
41
|
+
circle at center,
|
|
42
|
+
currentColor 1px,
|
|
43
|
+
transparent 1px
|
|
44
|
+
);
|
|
45
|
+
background-size: 22px 22px;
|
|
46
|
+
-webkit-mask-image: radial-gradient(
|
|
47
|
+
ellipse at center,
|
|
48
|
+
black 40%,
|
|
49
|
+
transparent 78%
|
|
50
|
+
);
|
|
51
|
+
mask-image: radial-gradient(ellipse at center, black 40%, transparent 78%);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@media (prefers-reduced-motion: reduce) {
|
|
55
|
+
.it-landing-rise,
|
|
56
|
+
.it-landing-float {
|
|
57
|
+
animation: none;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { useLayoutEffect, useRef, useState, type RefObject } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Observes an element's rendered size with a `ResizeObserver`.
|
|
5
|
+
*
|
|
6
|
+
* Used to make container-relative decisions (e.g. a table falling back to its
|
|
7
|
+
* cards view when the container itself is narrow, regardless of viewport
|
|
8
|
+
* width). The initial measurement runs in a layout effect so the first paint
|
|
9
|
+
* already has the real width and no incorrect layout flashes.
|
|
10
|
+
*
|
|
11
|
+
* @returns `ref` to attach to the element to measure, plus its current `width`
|
|
12
|
+
* and `height` in pixels (both `0` until the first measurement).
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* const { ref, width } = useElementSize<HTMLDivElement>();
|
|
16
|
+
* return <div ref={ref}>{width < 640 ? <Cards /> : <Table />}</div>;
|
|
17
|
+
*/
|
|
18
|
+
export function useElementSize<T extends HTMLElement>(): {
|
|
19
|
+
ref: RefObject<T | null>;
|
|
20
|
+
width: number;
|
|
21
|
+
height: number;
|
|
22
|
+
} {
|
|
23
|
+
const ref = useRef<T>(null);
|
|
24
|
+
const [size, setSize] = useState({ width: 0, height: 0 });
|
|
25
|
+
|
|
26
|
+
useLayoutEffect(() => {
|
|
27
|
+
const element = ref.current;
|
|
28
|
+
if (!element) return;
|
|
29
|
+
|
|
30
|
+
const update = () => {
|
|
31
|
+
const rect = element.getBoundingClientRect();
|
|
32
|
+
setSize({ width: rect.width, height: rect.height });
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
update();
|
|
36
|
+
|
|
37
|
+
const observer = new ResizeObserver(update);
|
|
38
|
+
observer.observe(element);
|
|
39
|
+
|
|
40
|
+
return () => observer.disconnect();
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
43
|
+
return { ref, width: size.width, height: size.height };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default useElementSize;
|