@bug-on/md3-react 2.0.3 → 3.0.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.
- package/.turbo/turbo-build.log +33 -0
- package/CHANGELOG.md +55 -0
- package/dist/index.css.d.ts +2 -0
- package/dist/index.d.mts +6127 -0
- package/dist/index.d.ts +6127 -71
- package/dist/index.js +1653 -614
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1566 -547
- package/dist/index.mjs.map +1 -1
- package/dist/material-symbols-cdn.css.d.ts +2 -0
- package/dist/material-symbols-self-hosted.css.d.ts +2 -0
- package/dist/typography.css.d.ts +2 -0
- package/package.json +22 -19
- package/scripts/copy-assets.js +82 -0
- package/src/assets/fonts/GoogleSansFlex-VariableFont.woff2 +0 -0
- package/src/assets/fonts/MaterialSymbolsOutlined-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
- package/src/assets/fonts/MaterialSymbolsRounded-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
- package/src/assets/fonts/MaterialSymbolsSharp-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
- package/src/assets/loading-indicator.svg +19 -0
- package/src/assets/material-symbols-cdn.css +65 -0
- package/src/assets/material-symbols-self-hosted.css +90 -0
- package/src/css.d.ts +20 -0
- package/src/hooks/useClickOutside.ts +37 -0
- package/src/hooks/useMediaQuery.ts +28 -0
- package/src/hooks/useRipple.ts +88 -0
- package/src/index.css +23 -0
- package/src/index.ts +349 -0
- package/src/lib/material-symbols-preconnect.tsx +82 -0
- package/src/lib/theme-utils.ts +180 -0
- package/src/lib/utils.ts +6 -0
- package/src/test/button.test.tsx +59 -0
- package/src/test/icon.test.tsx +91 -0
- package/src/test/loading-indicator.test.tsx +128 -0
- package/src/test/progress-indicator.test.tsx +306 -0
- package/src/test/setup.ts +80 -0
- package/src/test/typography.test.tsx +206 -0
- package/src/types/index.ts +7 -0
- package/src/types/md3.ts +31 -0
- package/src/ui/Text.tsx +60 -0
- package/src/ui/__snapshots__/divider.test.tsx.snap +63 -0
- package/src/ui/app-bar/app-bar-column.tsx +99 -0
- package/src/ui/app-bar/app-bar-item-button.tsx +71 -0
- package/src/ui/app-bar/app-bar-items.test.tsx +89 -0
- package/src/ui/app-bar/app-bar-overflow-indicator.tsx +108 -0
- package/src/ui/app-bar/app-bar-row.tsx +104 -0
- package/src/ui/app-bar/app-bar.test.tsx +87 -0
- package/src/ui/app-bar/app-bar.tokens.ts +223 -0
- package/src/ui/app-bar/app-bar.types.ts +441 -0
- package/src/ui/app-bar/bottom-app-bar.test.tsx +42 -0
- package/src/ui/app-bar/bottom-app-bar.tsx +84 -0
- package/src/ui/app-bar/docked-toolbar.test.tsx +34 -0
- package/src/ui/app-bar/docked-toolbar.tsx +54 -0
- package/src/ui/app-bar/flexible-app-bar.test.tsx +75 -0
- package/src/ui/app-bar/hooks/use-app-bar-scroll.ts +110 -0
- package/src/ui/app-bar/hooks/use-flexible-app-bar.ts +123 -0
- package/{dist/ui/app-bar/index.d.ts → src/ui/app-bar/index.ts} +35 -2
- package/src/ui/app-bar/large-flexible-app-bar.tsx +165 -0
- package/src/ui/app-bar/medium-flexible-app-bar.tsx +167 -0
- package/src/ui/app-bar/search-app-bar.test.tsx +49 -0
- package/src/ui/app-bar/search-app-bar.tsx +176 -0
- package/src/ui/app-bar/search-view.tsx +227 -0
- package/src/ui/app-bar/small-app-bar.test.tsx +48 -0
- package/src/ui/app-bar/small-app-bar.tsx +203 -0
- package/src/ui/badge.test.tsx +345 -0
- package/src/ui/badge.tsx +282 -0
- package/src/ui/button-group.test.tsx +71 -0
- package/src/ui/button-group.tsx +350 -0
- package/src/ui/button.test.tsx +297 -0
- package/src/ui/button.tsx +669 -0
- package/src/ui/card.test.tsx +187 -0
- package/src/ui/card.tsx +259 -0
- package/src/ui/checkbox.test.tsx +423 -0
- package/src/ui/checkbox.tsx +525 -0
- package/src/ui/chip.test.tsx +292 -0
- package/src/ui/chip.tsx +548 -0
- package/src/ui/code-block.tsx +219 -0
- package/src/ui/dialog.test.tsx +300 -0
- package/src/ui/dialog.tsx +384 -0
- package/src/ui/divider.test.tsx +314 -0
- package/src/ui/divider.tsx +412 -0
- package/src/ui/drawer.tsx +240 -0
- package/src/ui/fab-menu.test.tsx +494 -0
- package/src/ui/fab-menu.tsx +739 -0
- package/src/ui/fab.test.tsx +232 -0
- package/src/ui/fab.tsx +505 -0
- package/src/ui/icon-button.test.tsx +515 -0
- package/src/ui/icon-button.tsx +525 -0
- package/src/ui/icon.test.tsx +197 -0
- package/src/ui/icon.tsx +179 -0
- package/src/ui/loading-indicator.test.tsx +73 -0
- package/src/ui/loading-indicator.tsx +312 -0
- package/src/ui/menu/context-menu.tsx +275 -0
- package/src/ui/menu/index.ts +77 -0
- package/src/ui/menu/menu-animations.ts +102 -0
- package/src/ui/menu/menu-context.tsx +99 -0
- package/src/ui/menu/menu-divider.tsx +47 -0
- package/src/ui/menu/menu-group.tsx +200 -0
- package/src/ui/menu/menu-item.tsx +294 -0
- package/src/ui/menu/menu-tokens.ts +208 -0
- package/src/ui/menu/menu-types.ts +313 -0
- package/src/ui/menu/menu.test.tsx +624 -0
- package/src/ui/menu/menu.tsx +289 -0
- package/src/ui/menu/sub-menu.tsx +223 -0
- package/src/ui/menu/vertical-menu.tsx +382 -0
- package/src/ui/navigation-rail.test.tsx +404 -0
- package/src/ui/navigation-rail.tsx +604 -0
- package/src/ui/progress-indicator/circular.tsx +248 -0
- package/src/ui/progress-indicator/hooks.ts +51 -0
- package/{dist/ui/progress-indicator/index.d.ts → src/ui/progress-indicator/index.tsx} +20 -2
- package/src/ui/progress-indicator/linear-flat.tsx +83 -0
- package/src/ui/progress-indicator/linear-wavy.tsx +243 -0
- package/src/ui/progress-indicator/linear.tsx +143 -0
- package/src/ui/progress-indicator/types.ts +158 -0
- package/src/ui/progress-indicator/utils.ts +73 -0
- package/src/ui/radio-button.test.tsx +407 -0
- package/src/ui/radio-button.tsx +551 -0
- package/src/ui/ripple.test.tsx +72 -0
- package/src/ui/ripple.tsx +234 -0
- package/src/ui/scroll-area.test.tsx +58 -0
- package/src/ui/scroll-area.tsx +139 -0
- package/src/ui/search/animated-placeholder.tsx +145 -0
- package/src/ui/search/hooks/use-search-keyboard.test.ts +202 -0
- package/src/ui/search/hooks/use-search-keyboard.ts +104 -0
- package/src/ui/search/hooks/use-search-view-focus.test.ts +96 -0
- package/src/ui/search/hooks/use-search-view-focus.ts +24 -0
- package/src/ui/search/index.ts +44 -0
- package/src/ui/search/search-bar.tsx +220 -0
- package/src/ui/search/search-context.tsx +42 -0
- package/src/ui/search/search-view-docked.tsx +194 -0
- package/src/ui/search/search-view-fullscreen.tsx +247 -0
- package/src/ui/search/search.test.tsx +233 -0
- package/src/ui/search/search.tokens.ts +134 -0
- package/src/ui/search/search.tsx +131 -0
- package/src/ui/search/search.types.ts +154 -0
- package/src/ui/search/trailing-action.tsx +49 -0
- package/src/ui/shared/constants.ts +122 -0
- package/{dist/ui/shared/touch-target.d.ts → src/ui/shared/touch-target.tsx} +13 -1
- package/src/ui/slider/hooks/useSliderMath.ts +195 -0
- package/{dist/ui/slider/index.d.ts → src/ui/slider/index.ts} +12 -1
- package/src/ui/slider/range-slider.tsx +561 -0
- package/src/ui/slider/slider-thumb.tsx +379 -0
- package/src/ui/slider/slider-track.tsx +912 -0
- package/src/ui/slider/slider.tokens.ts +189 -0
- package/src/ui/slider/slider.tsx +259 -0
- package/src/ui/slider/slider.types.ts +288 -0
- package/src/ui/snackbar/index.ts +20 -0
- package/src/ui/snackbar/snackbar.test.tsx +338 -0
- package/src/ui/snackbar/snackbar.tsx +476 -0
- package/{dist/ui/switch/index.d.ts → src/ui/switch/index.ts} +1 -0
- package/src/ui/switch/switch.stories.tsx +309 -0
- package/src/ui/switch/switch.test.tsx +243 -0
- package/src/ui/switch/switch.tokens.ts +89 -0
- package/src/ui/switch/switch.tsx +504 -0
- package/src/ui/switch/switch.types.ts +62 -0
- package/{dist/ui/tabs/index.d.ts → src/ui/tabs/index.ts} +8 -1
- package/src/ui/tabs/tab.tsx +407 -0
- package/src/ui/tabs/tabs-content.tsx +89 -0
- package/src/ui/tabs/tabs-list.tsx +146 -0
- package/src/ui/tabs/tabs.test.tsx +290 -0
- package/src/ui/tabs/tabs.tokens.ts +121 -0
- package/src/ui/tabs/tabs.tsx +229 -0
- package/src/ui/tabs/tabs.types.ts +185 -0
- package/{dist/ui/text-field/index.d.ts → src/ui/text-field/index.ts} +8 -1
- package/src/ui/text-field/subcomponents/active-indicator.tsx +67 -0
- package/src/ui/text-field/subcomponents/floating-label.tsx +161 -0
- package/src/ui/text-field/subcomponents/leading-icon.tsx +46 -0
- package/src/ui/text-field/subcomponents/outline-container.tsx +170 -0
- package/src/ui/text-field/subcomponents/prefix-suffix.tsx +59 -0
- package/src/ui/text-field/subcomponents/supporting-text.tsx +145 -0
- package/src/ui/text-field/subcomponents/trailing-icon.tsx +199 -0
- package/src/ui/text-field/text-field.test.tsx +454 -0
- package/src/ui/text-field/text-field.tokens.ts +104 -0
- package/src/ui/text-field/text-field.tsx +548 -0
- package/src/ui/text-field/text-field.types.ts +180 -0
- package/src/ui/theme-provider/index.tsx +190 -0
- package/src/ui/toc.test.tsx +108 -0
- package/src/ui/toc.tsx +172 -0
- package/src/ui/tooltip/plain-tooltip.tsx +63 -0
- package/src/ui/tooltip/rich-tooltip.tsx +94 -0
- package/src/ui/tooltip/tooltip-box.tsx +266 -0
- package/src/ui/tooltip/tooltip-caret-shape.tsx +68 -0
- package/src/ui/tooltip/tooltip.tokens.ts +26 -0
- package/src/ui/tooltip/tooltip.types.ts +70 -0
- package/src/ui/tooltip/use-tooltip-position.ts +208 -0
- package/src/ui/tooltip/use-tooltip-state.ts +41 -0
- package/src/ui/typography/__tests__/typography.test.tsx +170 -0
- package/{dist/ui/typography/index.d.ts → src/ui/typography/index.ts} +21 -3
- package/src/ui/typography/type-scale-tokens.ts +205 -0
- package/src/ui/typography/typography-key-tokens.ts +43 -0
- package/src/ui/typography/typography-tokens.ts +360 -0
- package/src/ui/typography/typography.css +22 -0
- package/src/ui/typography/typography.tsx +559 -0
- package/test-render.tsx +4 -0
- package/test-shadow.html +26 -0
- package/test_output.txt +164 -0
- package/test_output_v2.txt +5 -0
- package/tsconfig.build.json +10 -0
- package/tsconfig.json +18 -0
- package/tsup.config.ts +20 -0
- package/vitest.config.ts +11 -0
- package/dist/hooks/useClickOutside.d.ts +0 -8
- package/dist/hooks/useMediaQuery.d.ts +0 -11
- package/dist/hooks/useRipple.d.ts +0 -26
- package/dist/lib/material-symbols-preconnect.d.ts +0 -42
- package/dist/lib/theme-utils.d.ts +0 -63
- package/dist/lib/utils.d.ts +0 -2
- package/dist/types/index.d.ts +0 -1
- package/dist/types/md3.d.ts +0 -14
- package/dist/ui/app-bar/app-bar-column.d.ts +0 -28
- package/dist/ui/app-bar/app-bar-item-button.d.ts +0 -16
- package/dist/ui/app-bar/app-bar-overflow-indicator.d.ts +0 -18
- package/dist/ui/app-bar/app-bar-row.d.ts +0 -36
- package/dist/ui/app-bar/app-bar.tokens.d.ts +0 -184
- package/dist/ui/app-bar/app-bar.types.d.ts +0 -392
- package/dist/ui/app-bar/bottom-app-bar.d.ts +0 -31
- package/dist/ui/app-bar/docked-toolbar.d.ts +0 -25
- package/dist/ui/app-bar/hooks/use-app-bar-scroll.d.ts +0 -42
- package/dist/ui/app-bar/hooks/use-flexible-app-bar.d.ts +0 -37
- package/dist/ui/app-bar/large-flexible-app-bar.d.ts +0 -26
- package/dist/ui/app-bar/medium-flexible-app-bar.d.ts +0 -28
- package/dist/ui/app-bar/search-app-bar.d.ts +0 -43
- package/dist/ui/app-bar/search-view.d.ts +0 -54
- package/dist/ui/app-bar/small-app-bar.d.ts +0 -37
- package/dist/ui/badge.d.ts +0 -125
- package/dist/ui/button-group.d.ts +0 -59
- package/dist/ui/button.d.ts +0 -148
- package/dist/ui/card.d.ts +0 -62
- package/dist/ui/checkbox.d.ts +0 -82
- package/dist/ui/chip.d.ts +0 -110
- package/dist/ui/code-block.d.ts +0 -14
- package/dist/ui/dialog.d.ts +0 -111
- package/dist/ui/divider.d.ts +0 -164
- package/dist/ui/drawer.d.ts +0 -39
- package/dist/ui/dropdown.d.ts +0 -29
- package/dist/ui/fab-menu.d.ts +0 -204
- package/dist/ui/fab.d.ts +0 -162
- package/dist/ui/icon-button.d.ts +0 -131
- package/dist/ui/icon.d.ts +0 -88
- package/dist/ui/loading-indicator.d.ts +0 -42
- package/dist/ui/navigation-rail.d.ts +0 -29
- package/dist/ui/progress-indicator/circular.d.ts +0 -3
- package/dist/ui/progress-indicator/hooks.d.ts +0 -3
- package/dist/ui/progress-indicator/linear-flat.d.ts +0 -10
- package/dist/ui/progress-indicator/linear-wavy.d.ts +0 -18
- package/dist/ui/progress-indicator/linear.d.ts +0 -3
- package/dist/ui/progress-indicator/types.d.ts +0 -151
- package/dist/ui/progress-indicator/utils.d.ts +0 -3
- package/dist/ui/radio-button.d.ts +0 -106
- package/dist/ui/ripple.d.ts +0 -126
- package/dist/ui/scroll-area.d.ts +0 -27
- package/dist/ui/search/animated-placeholder.d.ts +0 -54
- package/dist/ui/search/hooks/use-search-keyboard.d.ts +0 -32
- package/dist/ui/search/hooks/use-search-view-focus.d.ts +0 -6
- package/dist/ui/search/index.d.ts +0 -27
- package/dist/ui/search/search-bar.d.ts +0 -32
- package/dist/ui/search/search-context.d.ts +0 -24
- package/dist/ui/search/search-view-docked.d.ts +0 -25
- package/dist/ui/search/search-view-fullscreen.d.ts +0 -36
- package/dist/ui/search/search.d.ts +0 -50
- package/dist/ui/search/search.tokens.d.ts +0 -112
- package/dist/ui/search/search.types.d.ts +0 -131
- package/dist/ui/search/trailing-action.d.ts +0 -9
- package/dist/ui/shared/constants.d.ts +0 -86
- package/dist/ui/slider/hooks/useSliderMath.d.ts +0 -101
- package/dist/ui/slider/range-slider.d.ts +0 -47
- package/dist/ui/slider/slider-thumb.d.ts +0 -33
- package/dist/ui/slider/slider-track.d.ts +0 -25
- package/dist/ui/slider/slider.d.ts +0 -60
- package/dist/ui/slider/slider.tokens.d.ts +0 -151
- package/dist/ui/slider/slider.types.d.ts +0 -259
- package/dist/ui/snackbar/index.d.ts +0 -6
- package/dist/ui/snackbar/snackbar.d.ts +0 -197
- package/dist/ui/switch/switch.d.ts +0 -30
- package/dist/ui/switch/switch.stories.d.ts +0 -48
- package/dist/ui/switch/switch.tokens.d.ts +0 -67
- package/dist/ui/switch/switch.types.d.ts +0 -59
- package/dist/ui/tabs/tab.d.ts +0 -43
- package/dist/ui/tabs/tabs-content.d.ts +0 -36
- package/dist/ui/tabs/tabs-list.d.ts +0 -40
- package/dist/ui/tabs/tabs.d.ts +0 -60
- package/dist/ui/tabs/tabs.tokens.d.ts +0 -94
- package/dist/ui/tabs/tabs.types.d.ts +0 -172
- package/dist/ui/text-field/subcomponents/active-indicator.d.ts +0 -24
- package/dist/ui/text-field/subcomponents/floating-label.d.ts +0 -43
- package/dist/ui/text-field/subcomponents/leading-icon.d.ts +0 -23
- package/dist/ui/text-field/subcomponents/outline-container.d.ts +0 -42
- package/dist/ui/text-field/subcomponents/prefix-suffix.d.ts +0 -24
- package/dist/ui/text-field/subcomponents/supporting-text.d.ts +0 -37
- package/dist/ui/text-field/subcomponents/trailing-icon.d.ts +0 -41
- package/dist/ui/text-field/text-field.d.ts +0 -49
- package/dist/ui/text-field/text-field.tokens.d.ts +0 -76
- package/dist/ui/text-field/text-field.types.d.ts +0 -126
- package/dist/ui/theme-provider/index.d.ts +0 -48
- package/dist/ui/toc.d.ts +0 -80
- package/dist/ui/tooltip/plain-tooltip.d.ts +0 -2
- package/dist/ui/tooltip/rich-tooltip.d.ts +0 -2
- package/dist/ui/tooltip/tooltip-box.d.ts +0 -2
- package/dist/ui/tooltip/tooltip-caret-shape.d.ts +0 -9
- package/dist/ui/tooltip/tooltip.tokens.d.ts +0 -26
- package/dist/ui/tooltip/tooltip.types.d.ts +0 -56
- package/dist/ui/tooltip/use-tooltip-position.d.ts +0 -8
- package/dist/ui/tooltip/use-tooltip-state.d.ts +0 -2
- package/dist/ui/typography/type-scale-tokens.d.ts +0 -162
- package/dist/ui/typography/typography-key-tokens.d.ts +0 -40
- package/dist/ui/typography/typography-tokens.d.ts +0 -220
- package/dist/ui/typography/typography.d.ts +0 -265
- /package/{dist/hooks/index.d.ts → src/hooks/index.ts} +0 -0
- /package/{dist/ui/tooltip/index.d.ts → src/ui/tooltip/index.ts} +0 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createElement, useCallback, useEffect, useState } from "react";
|
|
4
|
+
import { cn } from "../lib/utils";
|
|
5
|
+
import { Button } from "./button";
|
|
6
|
+
import { Icon } from "./icon";
|
|
7
|
+
import { ScrollArea } from "./scroll-area";
|
|
8
|
+
|
|
9
|
+
const COPY_RESET_DELAY = 2000;
|
|
10
|
+
|
|
11
|
+
export interface CodeBlockProps {
|
|
12
|
+
/** Raw code string to display and copy. */
|
|
13
|
+
code: string;
|
|
14
|
+
/** Language label in the header (presentational only). @default "React" */
|
|
15
|
+
language?: string;
|
|
16
|
+
/** Additional CSS classes for the outer wrapper. */
|
|
17
|
+
className?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Pre-highlighted HTML from Shiki SSR.
|
|
20
|
+
* Use `codeToHtml` with `themes: { light, dark }` for dual-theme support.
|
|
21
|
+
*/
|
|
22
|
+
html?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ─── Sub-components ───────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
function CopyButton({
|
|
28
|
+
copied,
|
|
29
|
+
onCopy,
|
|
30
|
+
}: {
|
|
31
|
+
copied: boolean;
|
|
32
|
+
onCopy: () => void;
|
|
33
|
+
}) {
|
|
34
|
+
return (
|
|
35
|
+
<Button
|
|
36
|
+
type="button"
|
|
37
|
+
onClick={onCopy}
|
|
38
|
+
title="Copy code"
|
|
39
|
+
aria-label={copied ? "Code copied" : "Copy code"}
|
|
40
|
+
colorStyle="text"
|
|
41
|
+
className="h-8 px-2 gap-1.5"
|
|
42
|
+
>
|
|
43
|
+
{copied ? (
|
|
44
|
+
<>
|
|
45
|
+
<Icon
|
|
46
|
+
name="check"
|
|
47
|
+
size={14}
|
|
48
|
+
className="text-m3-primary"
|
|
49
|
+
aria-hidden="true"
|
|
50
|
+
/>
|
|
51
|
+
<span className="text-[10px] font-bold uppercase tracking-wider text-m3-primary">
|
|
52
|
+
Copied!
|
|
53
|
+
</span>
|
|
54
|
+
</>
|
|
55
|
+
) : (
|
|
56
|
+
<>
|
|
57
|
+
<Icon
|
|
58
|
+
name="content_copy"
|
|
59
|
+
size={14}
|
|
60
|
+
className="text-m3-on-surface-variant"
|
|
61
|
+
aria-hidden="true"
|
|
62
|
+
/>
|
|
63
|
+
<span className="text-[10px] font-bold uppercase tracking-wider text-m3-on-surface-variant">
|
|
64
|
+
Copy
|
|
65
|
+
</span>
|
|
66
|
+
</>
|
|
67
|
+
)}
|
|
68
|
+
</Button>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Mappings for HTML attributes to React props */
|
|
73
|
+
const ATTR_MAP: Record<string, string> = {
|
|
74
|
+
class: "className",
|
|
75
|
+
tabindex: "tabIndex",
|
|
76
|
+
readonly: "readOnly",
|
|
77
|
+
maxlength: "maxLength",
|
|
78
|
+
autocomplete: "autoComplete",
|
|
79
|
+
autofocus: "autoFocus",
|
|
80
|
+
contenteditable: "contentEditable",
|
|
81
|
+
spellcheck: "spellCheck",
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
function parseStyle(styleStr: string): React.CSSProperties {
|
|
85
|
+
const styleObj: Record<string, string> = {};
|
|
86
|
+
for (const s of styleStr.split(";")) {
|
|
87
|
+
const part = s.trim();
|
|
88
|
+
if (!part) continue;
|
|
89
|
+
const colonIndex = part.indexOf(":");
|
|
90
|
+
if (colonIndex === -1) continue;
|
|
91
|
+
|
|
92
|
+
const k = part.slice(0, colonIndex).trim();
|
|
93
|
+
const v = part.slice(colonIndex + 1).trim();
|
|
94
|
+
|
|
95
|
+
if (k.startsWith("--")) {
|
|
96
|
+
// CSS variables must be passed as-is
|
|
97
|
+
styleObj[k] = v;
|
|
98
|
+
} else {
|
|
99
|
+
// Standard properties should be camelCased for React
|
|
100
|
+
const key = k.replace(/-./g, (x) => x[1].toUpperCase());
|
|
101
|
+
styleObj[key] = v;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return styleObj as React.CSSProperties;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function mapDomToReact(node: Node, key: string | number): React.ReactNode {
|
|
108
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
109
|
+
return node.textContent;
|
|
110
|
+
}
|
|
111
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
112
|
+
const el = node as Element;
|
|
113
|
+
const tagName = el.tagName.toLowerCase();
|
|
114
|
+
const props: Record<string, unknown> = { key };
|
|
115
|
+
|
|
116
|
+
for (const attr of Array.from(el.attributes)) {
|
|
117
|
+
const propName = ATTR_MAP[attr.name] || attr.name;
|
|
118
|
+
if (propName === "style") {
|
|
119
|
+
props.style = parseStyle(attr.value);
|
|
120
|
+
} else {
|
|
121
|
+
props[propName] = attr.value;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return createElement(
|
|
126
|
+
tagName,
|
|
127
|
+
props,
|
|
128
|
+
Array.from(el.childNodes).map((child, i) => mapDomToReact(child, i)),
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function CodeContent({ html, code }: { html?: string; code: string }) {
|
|
135
|
+
const [parsedContent, setParsedContent] = useState<React.ReactNode>(null);
|
|
136
|
+
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
if (html) {
|
|
139
|
+
const parser = new DOMParser();
|
|
140
|
+
const doc = parser.parseFromString(html, "text/html");
|
|
141
|
+
const content = Array.from(doc.body.childNodes).map((node, i) =>
|
|
142
|
+
mapDomToReact(node, i),
|
|
143
|
+
);
|
|
144
|
+
setParsedContent(content);
|
|
145
|
+
}
|
|
146
|
+
}, [html]);
|
|
147
|
+
|
|
148
|
+
if (html) {
|
|
149
|
+
return (
|
|
150
|
+
<div
|
|
151
|
+
className={cn(
|
|
152
|
+
"text-sm font-mono",
|
|
153
|
+
"[&>pre]:bg-transparent! [&>pre]:p-0! [&>pre]:m-0!",
|
|
154
|
+
)}
|
|
155
|
+
>
|
|
156
|
+
{parsedContent}
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<pre className="text-sm font-mono text-m3-on-surface whitespace-pre">
|
|
163
|
+
{code}
|
|
164
|
+
</pre>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ─── CodeBlock ────────────────────────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
export function CodeBlock({
|
|
171
|
+
code,
|
|
172
|
+
language = "React",
|
|
173
|
+
className,
|
|
174
|
+
html,
|
|
175
|
+
}: CodeBlockProps) {
|
|
176
|
+
const [copied, setCopied] = useState(false);
|
|
177
|
+
|
|
178
|
+
const handleCopy = useCallback(async () => {
|
|
179
|
+
try {
|
|
180
|
+
await navigator.clipboard.writeText(code);
|
|
181
|
+
setCopied(true);
|
|
182
|
+
setTimeout(() => setCopied(false), COPY_RESET_DELAY);
|
|
183
|
+
} catch {
|
|
184
|
+
// Clipboard API unavailable — fail silently
|
|
185
|
+
}
|
|
186
|
+
}, [code]);
|
|
187
|
+
|
|
188
|
+
return (
|
|
189
|
+
<div
|
|
190
|
+
className={cn(
|
|
191
|
+
"rounded-m3-lg overflow-hidden max-w-full",
|
|
192
|
+
"bg-m3-surface-container-lowest border border-m3-outline-variant/60",
|
|
193
|
+
className,
|
|
194
|
+
)}
|
|
195
|
+
>
|
|
196
|
+
<div
|
|
197
|
+
className={cn(
|
|
198
|
+
"px-4 py-2 flex justify-between items-center",
|
|
199
|
+
"bg-m3-surface-container-low border-b border-m3-outline-variant/60",
|
|
200
|
+
)}
|
|
201
|
+
>
|
|
202
|
+
<span className="text-xs font-mono text-m3-on-surface-variant">
|
|
203
|
+
{language}
|
|
204
|
+
</span>
|
|
205
|
+
<CopyButton copied={copied} onCopy={handleCopy} />
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
<ScrollArea
|
|
209
|
+
type="hover"
|
|
210
|
+
orientation="both"
|
|
211
|
+
className="max-h-120 flex-col w-full min-w-0"
|
|
212
|
+
>
|
|
213
|
+
<div className="p-4 min-w-0 w-full">
|
|
214
|
+
<CodeContent html={html} code={code} />
|
|
215
|
+
</div>
|
|
216
|
+
</ScrollArea>
|
|
217
|
+
</div>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
4
|
+
import userEvent from "@testing-library/user-event";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
import { describe, expect, it, vi } from "vitest";
|
|
7
|
+
import {
|
|
8
|
+
Dialog,
|
|
9
|
+
DialogBody,
|
|
10
|
+
DialogContent,
|
|
11
|
+
DialogDescription,
|
|
12
|
+
DialogFooter,
|
|
13
|
+
DialogFullScreenContent,
|
|
14
|
+
DialogIcon,
|
|
15
|
+
DialogOverlay,
|
|
16
|
+
DialogPortal,
|
|
17
|
+
DialogTitle,
|
|
18
|
+
DialogTrigger,
|
|
19
|
+
} from "./dialog";
|
|
20
|
+
|
|
21
|
+
// Helper components for testing
|
|
22
|
+
const TestIcon = () => (
|
|
23
|
+
<svg data-testid="test-icon" aria-hidden="true" viewBox="0 0 24 24" />
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
interface ControlledDialogWrapperProps {
|
|
27
|
+
defaultOpen?: boolean;
|
|
28
|
+
onOpenChangeSpy?: (open: boolean) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const ControlledDialogWrapper = ({
|
|
32
|
+
defaultOpen = false,
|
|
33
|
+
onOpenChangeSpy,
|
|
34
|
+
}: ControlledDialogWrapperProps) => {
|
|
35
|
+
const [open, setOpen] = React.useState(defaultOpen);
|
|
36
|
+
return (
|
|
37
|
+
<Dialog
|
|
38
|
+
open={open}
|
|
39
|
+
onOpenChange={(v) => {
|
|
40
|
+
setOpen(v);
|
|
41
|
+
onOpenChangeSpy?.(v);
|
|
42
|
+
}}
|
|
43
|
+
>
|
|
44
|
+
<DialogTrigger data-testid="trigger">Open</DialogTrigger>
|
|
45
|
+
<DialogPortal open={open}>
|
|
46
|
+
<DialogOverlay />
|
|
47
|
+
<DialogContent aria-describedby={undefined}>
|
|
48
|
+
<DialogTitle>Title</DialogTitle>
|
|
49
|
+
</DialogContent>
|
|
50
|
+
</DialogPortal>
|
|
51
|
+
</Dialog>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
describe("Dialog", () => {
|
|
56
|
+
it("renders trigger button", () => {
|
|
57
|
+
render(
|
|
58
|
+
<Dialog>
|
|
59
|
+
<DialogTrigger data-testid="trigger">Open</DialogTrigger>
|
|
60
|
+
</Dialog>,
|
|
61
|
+
);
|
|
62
|
+
const trigger = screen.getByTestId("trigger");
|
|
63
|
+
expect(trigger).toBeInTheDocument();
|
|
64
|
+
expect(trigger).toHaveTextContent("Open");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("opens dialog on trigger click", async () => {
|
|
68
|
+
const user = userEvent.setup();
|
|
69
|
+
render(<ControlledDialogWrapper />);
|
|
70
|
+
|
|
71
|
+
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
|
72
|
+
|
|
73
|
+
await user.click(screen.getByTestId("trigger"));
|
|
74
|
+
|
|
75
|
+
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("closes dialog on close button click", async () => {
|
|
79
|
+
const user = userEvent.setup();
|
|
80
|
+
const handleOpenChange = vi.fn();
|
|
81
|
+
render(
|
|
82
|
+
<ControlledDialogWrapper
|
|
83
|
+
defaultOpen={true}
|
|
84
|
+
onOpenChangeSpy={handleOpenChange}
|
|
85
|
+
/>,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
|
89
|
+
|
|
90
|
+
const closeBtn = screen.getByLabelText("Close");
|
|
91
|
+
await user.click(closeBtn);
|
|
92
|
+
|
|
93
|
+
expect(handleOpenChange).toHaveBeenCalledWith(false);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("renders accessible title with role=heading", () => {
|
|
97
|
+
render(
|
|
98
|
+
<Dialog open={true}>
|
|
99
|
+
<DialogPortal open={true}>
|
|
100
|
+
<DialogContent aria-describedby={undefined}>
|
|
101
|
+
<DialogTitle>Dialog Headline</DialogTitle>
|
|
102
|
+
</DialogContent>
|
|
103
|
+
</DialogPortal>
|
|
104
|
+
</Dialog>,
|
|
105
|
+
);
|
|
106
|
+
const title = screen.getByRole("heading", { name: "Dialog Headline" });
|
|
107
|
+
expect(title).toBeInTheDocument();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("renders accessible description", () => {
|
|
111
|
+
render(
|
|
112
|
+
<Dialog open={true}>
|
|
113
|
+
<DialogPortal open={true}>
|
|
114
|
+
<DialogContent>
|
|
115
|
+
<DialogTitle>Title</DialogTitle>
|
|
116
|
+
<DialogDescription>Accessible description info.</DialogDescription>
|
|
117
|
+
</DialogContent>
|
|
118
|
+
</DialogPortal>
|
|
119
|
+
</Dialog>,
|
|
120
|
+
);
|
|
121
|
+
expect(
|
|
122
|
+
screen.getByText("Accessible description info."),
|
|
123
|
+
).toBeInTheDocument();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("hides close button when hideCloseButton=true", () => {
|
|
127
|
+
render(
|
|
128
|
+
<Dialog open={true}>
|
|
129
|
+
<DialogPortal open={true}>
|
|
130
|
+
<DialogContent hideCloseButton aria-describedby={undefined}>
|
|
131
|
+
<DialogTitle>Title</DialogTitle>
|
|
132
|
+
</DialogContent>
|
|
133
|
+
</DialogPortal>
|
|
134
|
+
</Dialog>,
|
|
135
|
+
);
|
|
136
|
+
expect(screen.queryByLabelText("Close")).not.toBeInTheDocument();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("calls onOpenChange when closed via Escape", async () => {
|
|
140
|
+
const handleOpenChange = vi.fn();
|
|
141
|
+
render(
|
|
142
|
+
<ControlledDialogWrapper
|
|
143
|
+
defaultOpen={true}
|
|
144
|
+
onOpenChangeSpy={handleOpenChange}
|
|
145
|
+
/>,
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
fireEvent.keyDown(document, { key: "Escape" });
|
|
149
|
+
expect(handleOpenChange).toHaveBeenCalledWith(false);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("renders DialogIcon slot", () => {
|
|
153
|
+
render(
|
|
154
|
+
<Dialog open={true}>
|
|
155
|
+
<DialogPortal open={true}>
|
|
156
|
+
<DialogContent aria-describedby={undefined}>
|
|
157
|
+
<DialogIcon>
|
|
158
|
+
<TestIcon />
|
|
159
|
+
</DialogIcon>
|
|
160
|
+
<DialogTitle>Title</DialogTitle>
|
|
161
|
+
</DialogContent>
|
|
162
|
+
</DialogPortal>
|
|
163
|
+
</Dialog>,
|
|
164
|
+
);
|
|
165
|
+
const icon = screen.getByTestId("test-icon");
|
|
166
|
+
expect(icon).toBeInTheDocument();
|
|
167
|
+
const iconWrapper = icon.parentElement;
|
|
168
|
+
expect(iconWrapper).toHaveAttribute("aria-hidden", "true");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("renders DialogBody with overflow-y-auto", () => {
|
|
172
|
+
render(
|
|
173
|
+
<Dialog open={true}>
|
|
174
|
+
<DialogPortal open={true}>
|
|
175
|
+
<DialogContent aria-describedby={undefined}>
|
|
176
|
+
<DialogTitle>Title</DialogTitle>
|
|
177
|
+
<DialogBody data-testid="body">Content area</DialogBody>
|
|
178
|
+
</DialogContent>
|
|
179
|
+
</DialogPortal>
|
|
180
|
+
</Dialog>,
|
|
181
|
+
);
|
|
182
|
+
const body = screen.getByTestId("body");
|
|
183
|
+
expect(body).toBeInTheDocument();
|
|
184
|
+
// DialogBody is a flex container; ScrollArea inside handles overflow
|
|
185
|
+
expect(body.className).toContain("flex");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("renders DialogFooter with end-aligned buttons", () => {
|
|
189
|
+
render(
|
|
190
|
+
<Dialog open={true}>
|
|
191
|
+
<DialogPortal open={true}>
|
|
192
|
+
<DialogContent aria-describedby={undefined}>
|
|
193
|
+
<DialogTitle>Title</DialogTitle>
|
|
194
|
+
<DialogFooter data-testid="footer">
|
|
195
|
+
<button type="button">Action</button>
|
|
196
|
+
</DialogFooter>
|
|
197
|
+
</DialogContent>
|
|
198
|
+
</DialogPortal>
|
|
199
|
+
</Dialog>,
|
|
200
|
+
);
|
|
201
|
+
const footer = screen.getByTestId("footer");
|
|
202
|
+
expect(footer).toBeInTheDocument();
|
|
203
|
+
expect(footer.className).toContain("justify-end");
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe("DialogFullScreenContent", () => {
|
|
208
|
+
it("renders full-screen dialog covering viewport", () => {
|
|
209
|
+
render(
|
|
210
|
+
<Dialog open={true}>
|
|
211
|
+
<DialogPortal open={true}>
|
|
212
|
+
<DialogFullScreenContent aria-describedby={undefined}>
|
|
213
|
+
<p>Full screen body</p>
|
|
214
|
+
</DialogFullScreenContent>
|
|
215
|
+
</DialogPortal>
|
|
216
|
+
</Dialog>,
|
|
217
|
+
);
|
|
218
|
+
const dialog = screen.getByRole("dialog");
|
|
219
|
+
expect(dialog).toBeInTheDocument();
|
|
220
|
+
expect(dialog.className).toContain("fixed inset-0 z-50 w-full h-full");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("renders title in top app bar", () => {
|
|
224
|
+
render(
|
|
225
|
+
<Dialog open={true}>
|
|
226
|
+
<DialogPortal open={true}>
|
|
227
|
+
<DialogFullScreenContent
|
|
228
|
+
title="My Form Title"
|
|
229
|
+
aria-describedby={undefined}
|
|
230
|
+
>
|
|
231
|
+
<p>Full screen body</p>
|
|
232
|
+
</DialogFullScreenContent>
|
|
233
|
+
</DialogPortal>
|
|
234
|
+
</Dialog>,
|
|
235
|
+
);
|
|
236
|
+
expect(screen.getByText("My Form Title")).toBeInTheDocument();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("renders action button in top bar when actionLabel provided", () => {
|
|
240
|
+
render(
|
|
241
|
+
<Dialog open={true}>
|
|
242
|
+
<DialogPortal open={true}>
|
|
243
|
+
<DialogFullScreenContent
|
|
244
|
+
title="Title"
|
|
245
|
+
actionLabel="Save Form"
|
|
246
|
+
onAction={vi.fn()}
|
|
247
|
+
aria-describedby={undefined}
|
|
248
|
+
>
|
|
249
|
+
<p>Full screen body</p>
|
|
250
|
+
</DialogFullScreenContent>
|
|
251
|
+
</DialogPortal>
|
|
252
|
+
</Dialog>,
|
|
253
|
+
);
|
|
254
|
+
const actionBtn = screen.getByRole("button", { name: "Save Form" });
|
|
255
|
+
expect(actionBtn).toBeInTheDocument();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("calls onAction callback when action button clicked", async () => {
|
|
259
|
+
const user = userEvent.setup();
|
|
260
|
+
const handleAction = vi.fn();
|
|
261
|
+
render(
|
|
262
|
+
<Dialog open={true}>
|
|
263
|
+
<DialogPortal open={true}>
|
|
264
|
+
<DialogFullScreenContent
|
|
265
|
+
title="Title"
|
|
266
|
+
actionLabel="Save Data"
|
|
267
|
+
onAction={handleAction}
|
|
268
|
+
aria-describedby={undefined}
|
|
269
|
+
>
|
|
270
|
+
<p>Full screen body</p>
|
|
271
|
+
</DialogFullScreenContent>
|
|
272
|
+
</DialogPortal>
|
|
273
|
+
</Dialog>,
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
const actionBtn = screen.getByRole("button", { name: "Save Data" });
|
|
277
|
+
await user.click(actionBtn);
|
|
278
|
+
expect(handleAction).toHaveBeenCalledTimes(1);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("renders divider when showDivider=true", () => {
|
|
282
|
+
render(
|
|
283
|
+
<Dialog open={true}>
|
|
284
|
+
<DialogPortal open={true}>
|
|
285
|
+
<DialogFullScreenContent
|
|
286
|
+
title="Title"
|
|
287
|
+
showDivider
|
|
288
|
+
aria-describedby={undefined}
|
|
289
|
+
>
|
|
290
|
+
<p>Full screen body</p>
|
|
291
|
+
</DialogFullScreenContent>
|
|
292
|
+
</DialogPortal>
|
|
293
|
+
</Dialog>,
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
const divider = document.querySelector("hr");
|
|
297
|
+
expect(divider).toBeInTheDocument();
|
|
298
|
+
expect(divider?.className).toContain("border-m3-outline-variant");
|
|
299
|
+
});
|
|
300
|
+
});
|