@seed-design/lynx-react 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/lib/components/Checkbox/Checkbox.d.ts +26 -0
  2. package/lib/components/Checkbox/Checkbox.d.ts.map +1 -1
  3. package/lib/components/Checkbox/Checkbox.jsx +1 -1
  4. package/lib/components/Icon/Icon.d.ts +5 -0
  5. package/lib/components/Icon/Icon.d.ts.map +1 -1
  6. package/lib/components/Icon/Icon.jsx +14 -3
  7. package/lib/components/MannerTemp/MannerTemp.d.ts +19 -0
  8. package/lib/components/MannerTemp/MannerTemp.d.ts.map +1 -0
  9. package/lib/components/MannerTemp/MannerTemp.jsx +47 -0
  10. package/lib/components/MannerTemp/index.d.ts +2 -0
  11. package/lib/components/MannerTemp/index.d.ts.map +1 -0
  12. package/lib/components/MannerTempBadge/MannerTempBadge.d.ts +13 -0
  13. package/lib/components/MannerTempBadge/MannerTempBadge.d.ts.map +1 -0
  14. package/lib/components/MannerTempBadge/MannerTempBadge.jsx +15 -0
  15. package/lib/components/MannerTempBadge/index.d.ts +2 -0
  16. package/lib/components/MannerTempBadge/index.d.ts.map +1 -0
  17. package/lib/components/NotificationBadge/NotificationBadge.d.ts +18 -0
  18. package/lib/components/NotificationBadge/NotificationBadge.d.ts.map +1 -0
  19. package/lib/components/NotificationBadge/NotificationBadge.jsx +34 -0
  20. package/lib/components/NotificationBadge/index.d.ts +2 -0
  21. package/lib/components/NotificationBadge/index.d.ts.map +1 -0
  22. package/lib/components/PageBanner/PageBanner.d.ts +35 -0
  23. package/lib/components/PageBanner/PageBanner.d.ts.map +1 -0
  24. package/lib/components/PageBanner/PageBanner.jsx +150 -0
  25. package/lib/components/PageBanner/PageBanner.namespace.d.ts +2 -0
  26. package/lib/components/PageBanner/PageBanner.namespace.d.ts.map +1 -0
  27. package/lib/components/PageBanner/PageBanner.namespace.js +14 -0
  28. package/lib/components/PageBanner/index.d.ts +3 -0
  29. package/lib/components/PageBanner/index.d.ts.map +1 -0
  30. package/lib/components/RadioGroup/RadioGroup.d.ts +9 -0
  31. package/lib/components/RadioGroup/RadioGroup.d.ts.map +1 -1
  32. package/lib/components/RadioGroup/RadioGroup.jsx +1 -1
  33. package/lib/components/SegmentedControl/SegmentedControl.d.ts +27 -0
  34. package/lib/components/SegmentedControl/SegmentedControl.d.ts.map +1 -0
  35. package/lib/components/SegmentedControl/SegmentedControl.jsx +120 -0
  36. package/lib/components/SegmentedControl/SegmentedControl.namespace.d.ts +2 -0
  37. package/lib/components/SegmentedControl/SegmentedControl.namespace.d.ts.map +1 -0
  38. package/lib/components/SegmentedControl/SegmentedControl.namespace.js +10 -0
  39. package/lib/components/SegmentedControl/index.d.ts +3 -0
  40. package/lib/components/SegmentedControl/index.d.ts.map +1 -0
  41. package/lib/components/SelectBox/CheckSelectBox.namespace.d.ts +2 -0
  42. package/lib/components/SelectBox/CheckSelectBox.namespace.d.ts.map +1 -0
  43. package/lib/components/SelectBox/CheckSelectBox.namespace.js +17 -0
  44. package/lib/components/SelectBox/RadioSelectBox.namespace.d.ts +2 -0
  45. package/lib/components/SelectBox/RadioSelectBox.namespace.d.ts.map +1 -0
  46. package/lib/components/SelectBox/RadioSelectBox.namespace.js +15 -0
  47. package/lib/components/SelectBox/SelectBox.d.ts +85 -0
  48. package/lib/components/SelectBox/SelectBox.d.ts.map +1 -0
  49. package/lib/components/SelectBox/SelectBox.jsx +237 -0
  50. package/lib/components/SelectBox/index.d.ts +4 -0
  51. package/lib/components/SelectBox/index.d.ts.map +1 -0
  52. package/lib/components/index.d.ts +6 -0
  53. package/lib/components/index.d.ts.map +1 -1
  54. package/lib/hooks/useIconColor.d.ts +1 -0
  55. package/lib/hooks/useIconColor.d.ts.map +1 -1
  56. package/lib/hooks/useIconColor.js +4 -1
  57. package/lib/index.js +11 -1
  58. package/package.json +3 -3
  59. package/src/components/Checkbox/Checkbox.tsx +1 -1
  60. package/src/components/Icon/Icon.test.tsx +25 -1
  61. package/src/components/Icon/Icon.tsx +22 -1
  62. package/src/components/MannerTemp/MannerTemp.test.tsx +41 -0
  63. package/src/components/MannerTemp/MannerTemp.tsx +85 -0
  64. package/src/components/MannerTemp/index.ts +1 -0
  65. package/src/components/MannerTempBadge/MannerTempBadge.test.tsx +35 -0
  66. package/src/components/MannerTempBadge/MannerTempBadge.tsx +34 -0
  67. package/src/components/MannerTempBadge/index.ts +1 -0
  68. package/src/components/NotificationBadge/NotificationBadge.test.tsx +28 -0
  69. package/src/components/NotificationBadge/NotificationBadge.tsx +81 -0
  70. package/src/components/NotificationBadge/index.ts +1 -0
  71. package/src/components/PageBanner/PageBanner.namespace.ts +16 -0
  72. package/src/components/PageBanner/PageBanner.test.tsx +124 -0
  73. package/src/components/PageBanner/PageBanner.tsx +340 -0
  74. package/src/components/PageBanner/index.ts +18 -0
  75. package/src/components/RadioGroup/RadioGroup.tsx +1 -1
  76. package/src/components/SegmentedControl/SegmentedControl.namespace.ts +8 -0
  77. package/src/components/SegmentedControl/SegmentedControl.test.tsx +81 -0
  78. package/src/components/SegmentedControl/SegmentedControl.tsx +236 -0
  79. package/src/components/SegmentedControl/index.ts +10 -0
  80. package/src/components/SelectBox/CheckSelectBox.namespace.ts +22 -0
  81. package/src/components/SelectBox/RadioSelectBox.namespace.ts +18 -0
  82. package/src/components/SelectBox/SelectBox.test.tsx +169 -0
  83. package/src/components/SelectBox/SelectBox.tsx +533 -0
  84. package/src/components/SelectBox/index.ts +3 -0
  85. package/src/components/index.ts +6 -0
  86. package/src/hooks/useIconColor.ts +6 -1
package/lib/index.js CHANGED
@@ -27,8 +27,18 @@ import { Divider } from "./components/Divider/Divider.jsx";
27
27
  import { FieldCharacterCount, FieldDescription, FieldErrorMessage, FieldFooter, FieldHeader, FieldIndicatorText, FieldLabel, FieldRequiredIndicator, FieldRoot } from "./components/Field/Field.jsx";
28
28
  import { Field_namespace_exports } from "./components/Field/Field.namespace.js";
29
29
  import { KeyboardAvoidingScrollView } from "./components/KeyboardAvoidingScrollView/KeyboardAvoidingScrollView.jsx";
30
+ import { MannerTemp, MannerTempEmote } from "./components/MannerTemp/MannerTemp.jsx";
31
+ import { MannerTempBadge } from "./components/MannerTempBadge/MannerTempBadge.jsx";
32
+ import { NotificationBadge, NotificationBadgePositioner } from "./components/NotificationBadge/NotificationBadge.jsx";
33
+ import { PageBannerBody, PageBannerButton, PageBannerCloseButton, PageBannerContent, PageBannerDescription, PageBannerRoot, PageBannerTitle } from "./components/PageBanner/PageBanner.jsx";
34
+ import { PageBanner_namespace_exports } from "./components/PageBanner/PageBanner.namespace.js";
30
35
  import { RadioGroupItem, RadioGroupItemControl, RadioGroupItemIndicator, RadioGroupItemLabel, RadioGroupRoot } from "./components/RadioGroup/RadioGroup.jsx";
31
36
  import { RadioGroup_namespace_exports } from "./components/RadioGroup/RadioGroup.namespace.js";
37
+ import { SegmentedControlIndicator, SegmentedControlItem, SegmentedControlRoot } from "./components/SegmentedControl/SegmentedControl.jsx";
38
+ import { SegmentedControl_namespace_exports } from "./components/SegmentedControl/SegmentedControl.namespace.js";
39
+ import { CheckSelectBoxBody, CheckSelectBoxCheckmarkControl, CheckSelectBoxCheckmarkIcon, CheckSelectBoxContent, CheckSelectBoxDescription, CheckSelectBoxFooter, CheckSelectBoxGroup, CheckSelectBoxLabel, CheckSelectBoxRoot, CheckSelectBoxTrigger, RadioSelectBoxBody, RadioSelectBoxContent, RadioSelectBoxDescription, RadioSelectBoxFooter, RadioSelectBoxGroup, RadioSelectBoxItem, RadioSelectBoxLabel, RadioSelectBoxTrigger } from "./components/SelectBox/SelectBox.jsx";
40
+ import { CheckSelectBox_namespace_exports } from "./components/SelectBox/CheckSelectBox.namespace.js";
41
+ import { RadioSelectBox_namespace_exports } from "./components/SelectBox/RadioSelectBox.namespace.js";
32
42
  import { HStack, VStack } from "./components/Stack/Stack.jsx";
33
43
  import { SwitchControl, SwitchLabel, SwitchRoot, SwitchThumb } from "./components/Switch/Switch.jsx";
34
44
  import { Switch_namespace_exports } from "./components/Switch/Switch.namespace.js";
@@ -42,4 +52,4 @@ import { useTextFieldWithGraphemes } from "./components/TextField/useTextFieldWi
42
52
  import { TextField_namespace_exports } from "./components/TextField/TextField.namespace.js";
43
53
  import { getSeedClassName } from "./utils/get-seed-class-name.js";
44
54
  import { useSeedClassName } from "./hooks/useSeedClassName.js";
45
- export { Accordion_namespace_exports as Accordion, AccordionBody, AccordionContent, AccordionDescription, AccordionHeader, AccordionItem, AccordionPrefix, AccordionRoot, AccordionSuffixIcon, AccordionTitle, AccordionTrigger, ActionButton, AppBar_namespace_exports as AppBar, AppBarIconButton, AppBarLeft, AppBarMain, AppBarRight, AppBarRoot, AppBarSlot, AppBarSubtitle, AppBarTitle, Badge, BottomSheet_namespace_exports as BottomSheet, BottomSheetBackdrop, BottomSheetBody, BottomSheetContent, BottomSheetDescription, BottomSheetFooter, BottomSheetHandle, BottomSheetHeader, BottomSheetPositioner, BottomSheetRoot, BottomSheetTitle, BottomSheetTrigger, Box, Callout_namespace_exports as Callout, CalloutCloseButton, CalloutContent, CalloutDescription, CalloutLink, CalloutRoot, CalloutTitle, Checkbox_namespace_exports as Checkbox, CheckboxControl, CheckboxGroup, CheckboxIndicator, CheckboxLabel, CheckboxRoot, Chip_namespace_exports as Chip, ChipButton, ChipLabel, ChipPrefixAvatar, ChipPrefixIcon, ChipRadioItem, ChipRadioRoot, ChipRoot, ChipSuffixIcon, ChipToggle, Divider, Field_namespace_exports as Field, FieldCharacterCount, FieldDescription, FieldErrorMessage, FieldFooter, FieldHeader, FieldIndicatorText, FieldLabel, FieldRequiredIndicator, FieldRoot, HStack, Icon, KeyboardAvoidingScrollView, PrefixIcon, ProgressCircle_namespace_exports as ProgressCircle, ProgressCircleRange, ProgressCircleRoot, RadioGroup_namespace_exports as RadioGroup, RadioGroupItem, RadioGroupItemControl, RadioGroupItemIndicator, RadioGroupItemLabel, RadioGroupRoot, SuffixIcon, Switch_namespace_exports as Switch, SwitchControl, SwitchLabel, SwitchRoot, SwitchThumb, Tabs_namespace_exports as Tabs, TabsCarousel, TabsCarouselCamera, TabsContent, TabsIndicator, TabsList, TabsRoot, TabsTrigger, TagGroup_namespace_exports as TagGroup, TagGroupItem, TagGroupItemLabel, TagGroupRoot, Text, TextField_namespace_exports as TextField, TextFieldInput, TextFieldPrefixIcon, TextFieldPrefixText, TextFieldRoot, TextFieldSuffixIcon, TextFieldSuffixText, TextFieldTextarea, VStack, capitalize, createSlotRecipeContext, getSeedClassName, resolveRecipeToken, splitMultipleVariantsProps, useControllableState, useIconColor, usePressTap, useSafeArea, useSeedClassName, useTextFieldWithGraphemes };
55
+ export { Accordion_namespace_exports as Accordion, AccordionBody, AccordionContent, AccordionDescription, AccordionHeader, AccordionItem, AccordionPrefix, AccordionRoot, AccordionSuffixIcon, AccordionTitle, AccordionTrigger, ActionButton, AppBar_namespace_exports as AppBar, AppBarIconButton, AppBarLeft, AppBarMain, AppBarRight, AppBarRoot, AppBarSlot, AppBarSubtitle, AppBarTitle, Badge, BottomSheet_namespace_exports as BottomSheet, BottomSheetBackdrop, BottomSheetBody, BottomSheetContent, BottomSheetDescription, BottomSheetFooter, BottomSheetHandle, BottomSheetHeader, BottomSheetPositioner, BottomSheetRoot, BottomSheetTitle, BottomSheetTrigger, Box, Callout_namespace_exports as Callout, CalloutCloseButton, CalloutContent, CalloutDescription, CalloutLink, CalloutRoot, CalloutTitle, CheckSelectBox_namespace_exports as CheckSelectBox, CheckSelectBoxBody, CheckSelectBoxCheckmarkControl, CheckSelectBoxCheckmarkIcon, CheckSelectBoxContent, CheckSelectBoxDescription, CheckSelectBoxFooter, CheckSelectBoxGroup, CheckSelectBoxLabel, CheckSelectBoxRoot, CheckSelectBoxTrigger, Checkbox_namespace_exports as Checkbox, CheckboxControl, CheckboxGroup, CheckboxIndicator, CheckboxLabel, CheckboxRoot, Chip_namespace_exports as Chip, ChipButton, ChipLabel, ChipPrefixAvatar, ChipPrefixIcon, ChipRadioItem, ChipRadioRoot, ChipRoot, ChipSuffixIcon, ChipToggle, Divider, Field_namespace_exports as Field, FieldCharacterCount, FieldDescription, FieldErrorMessage, FieldFooter, FieldHeader, FieldIndicatorText, FieldLabel, FieldRequiredIndicator, FieldRoot, HStack, Icon, KeyboardAvoidingScrollView, MannerTemp, MannerTempBadge, MannerTempEmote, NotificationBadge, NotificationBadgePositioner, PageBanner_namespace_exports as PageBanner, PageBannerBody, PageBannerButton, PageBannerCloseButton, PageBannerContent, PageBannerDescription, PageBannerRoot, PageBannerTitle, PrefixIcon, ProgressCircle_namespace_exports as ProgressCircle, ProgressCircleRange, ProgressCircleRoot, RadioGroup_namespace_exports as RadioGroup, RadioGroupItem, RadioGroupItemControl, RadioGroupItemIndicator, RadioGroupItemLabel, RadioGroupRoot, RadioSelectBox_namespace_exports as RadioSelectBox, RadioSelectBoxBody, RadioSelectBoxContent, RadioSelectBoxDescription, RadioSelectBoxFooter, RadioSelectBoxGroup, RadioSelectBoxItem, RadioSelectBoxLabel, RadioSelectBoxTrigger, SegmentedControl_namespace_exports as SegmentedControl, SegmentedControlIndicator, SegmentedControlItem, SegmentedControlRoot, SuffixIcon, Switch_namespace_exports as Switch, SwitchControl, SwitchLabel, SwitchRoot, SwitchThumb, Tabs_namespace_exports as Tabs, TabsCarousel, TabsCarouselCamera, TabsContent, TabsIndicator, TabsList, TabsRoot, TabsTrigger, TagGroup_namespace_exports as TagGroup, TagGroupItem, TagGroupItemLabel, TagGroupRoot, Text, TextField_namespace_exports as TextField, TextFieldInput, TextFieldPrefixIcon, TextFieldPrefixText, TextFieldRoot, TextFieldSuffixIcon, TextFieldSuffixText, TextFieldTextarea, VStack, capitalize, createSlotRecipeContext, getSeedClassName, resolveRecipeToken, splitMultipleVariantsProps, useControllableState, useIconColor, usePressTap, useSafeArea, useSeedClassName, useTextFieldWithGraphemes };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seed-design/lynx-react",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/daangn/seed-design.git",
@@ -34,12 +34,12 @@
34
34
  "peerDependencies": {
35
35
  "@lynx-js/react": ">=0.117.0",
36
36
  "@lynx-js/types": ">=3.7.0",
37
- "@seed-design/lynx-css": "0.0.0 || >=0.8.0 <1.0.0"
37
+ "@seed-design/lynx-css": "0.0.0 || >=0.10.0 <1.0.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@lynx-js/react": "^0.117.0",
41
41
  "@lynx-js/types": "^3.7.0",
42
- "@seed-design/lynx-css": "0.9.3",
42
+ "@seed-design/lynx-css": "0.10.0",
43
43
  "@testing-library/jest-dom": "^6.9.1",
44
44
  "jsdom": "^29.0.2",
45
45
  "preact": "npm:@lynx-js/internal-preact@10.28.4-ee7bb26",
@@ -47,7 +47,7 @@ interface CheckboxContextValue {
47
47
 
48
48
  const CheckboxContext = React.createContext<CheckboxContextValue | null>(null);
49
49
 
50
- function useCheckboxContext(consumer: string): CheckboxContextValue {
50
+ export function useCheckboxContext(consumer: string): CheckboxContextValue {
51
51
  const ctx = React.useContext(CheckboxContext);
52
52
  if (!ctx) {
53
53
  throw new Error(`<${consumer}/> must be rendered inside <CheckboxRoot/>.`);
@@ -4,7 +4,7 @@ import type { MainThread } from "@lynx-js/types";
4
4
  import { describe, expect, it } from "vitest";
5
5
 
6
6
  import type { LynxIconElementProps } from "../../types";
7
- import { InternalIcon } from "./Icon";
7
+ import { InternalIcon, PrefixIcon } from "./Icon";
8
8
 
9
9
  const TestIcon = React.forwardRef<
10
10
  MainThread.Element,
@@ -20,6 +20,15 @@ const TestIcon = React.forwardRef<
20
20
  });
21
21
  TestIcon.displayName = "TestIcon";
22
22
 
23
+ const MulticolorTestIcon = React.forwardRef<
24
+ MainThread.Element,
25
+ LynxIconElementProps & { children?: React.ReactNode }
26
+ >((props, ref) => <TestIcon {...props} ref={ref} />);
27
+ MulticolorTestIcon.displayName = "MulticolorTestIcon";
28
+ Object.assign(MulticolorTestIcon, {
29
+ [Symbol.for("@seed-design/multicolor-icon")]: true,
30
+ });
31
+
23
32
  function getSourceAndImage() {
24
33
  const source = elementTree.root?.querySelector<HTMLElement>("view");
25
34
  const image = source?.querySelector<HTMLElement>("image");
@@ -127,3 +136,18 @@ describe("InternalIcon", () => {
127
136
  expect(getSourceAndImage().image.getAttribute("tint-color")).toBe("rgb(73, 80, 88)");
128
137
  });
129
138
  });
139
+
140
+ describe("PrefixIcon", () => {
141
+ it("does not tint marked multicolor icons", async () => {
142
+ const { frames } = installMainThreadStyleMocks(() => "rgb(31, 35, 40)");
143
+
144
+ render(<PrefixIcon icon={<MulticolorTestIcon />} />, {
145
+ enableMainThread: true,
146
+ enableBackgroundThread: true,
147
+ });
148
+ await waitSchedule();
149
+
150
+ expect(frames.size).toBe(0);
151
+ expect(getSourceAndImage().image.getAttribute("tint-color")).toBeNull();
152
+ });
153
+ });
@@ -93,23 +93,42 @@ export function IconSlotProvider({
93
93
  ////////////////////////////////////////////////////////////////////////////////////
94
94
 
95
95
  const iconSlotMarker = Symbol.for("@seed-design/lynx-react/icon-slot");
96
+ const seedMulticolorIconMarker = Symbol.for("@seed-design/multicolor-icon");
96
97
 
97
98
  interface IconSlotComponent {
98
99
  [iconSlotMarker]?: IconSlotName;
99
100
  }
100
101
 
102
+ interface MulticolorIconComponent {
103
+ [seedMulticolorIconMarker]?: boolean;
104
+ }
105
+
101
106
  export function getIconSlotName(node: React.ReactNode): IconSlotName | null {
102
107
  if (!isValidElement(node)) return null;
103
108
 
104
109
  return ((node.type as IconSlotComponent)[iconSlotMarker] ?? null) as IconSlotName | null;
105
110
  }
106
111
 
112
+ function isMulticolorIcon(node: React.ReactNode): boolean {
113
+ if (!isValidElement(node)) return false;
114
+ if (node.type == null || (typeof node.type !== "function" && typeof node.type !== "object")) {
115
+ return false;
116
+ }
117
+
118
+ return (node.type as MulticolorIconComponent)[seedMulticolorIconMarker] === true;
119
+ }
120
+
107
121
  ////////////////////////////////////////////////////////////////////////////////////
108
122
 
109
123
  export interface IconProps extends LynxStyledElementProps {
110
124
  icon: ReactElement<LynxIconElementProps>;
111
125
  size?: StyleProps["height"] | number;
112
126
  color?: StyleProps["color"];
127
+ /**
128
+ * 자동 감지되지 않는 멀티컬러 아이콘의 원본 색상을 유지합니다.
129
+ * @default false
130
+ */
131
+ multicolor?: boolean;
113
132
  }
114
133
 
115
134
  export interface PrefixIconProps extends IconProps {}
@@ -151,6 +170,7 @@ const IconSlotBase = React.forwardRef<unknown, IconSlotBaseProps>((props, ref) =
151
170
  style,
152
171
  size,
153
172
  color,
173
+ multicolor = false,
154
174
  children: _children,
155
175
  ...nativeProps
156
176
  } = props;
@@ -159,9 +179,10 @@ const IconSlotBase = React.forwardRef<unknown, IconSlotBaseProps>((props, ref) =
159
179
  const sourceRef = useMainThreadRef<MainThread.Element>(null);
160
180
  const slotClassName = slot != null ? context?.classNames?.[slot] : undefined;
161
181
  const styleColor = getStyleColor(style);
182
+ const shouldPreserveOriginalColor = multicolor || isMulticolorIcon(icon);
162
183
  const iconColor = useIconColor(
163
184
  [baseClassName, slotClassName, className, size, color, styleColor, ...(context?.deps ?? [])],
164
- { sourceRef },
185
+ { sourceRef, enabled: !shouldPreserveOriginalColor },
165
186
  );
166
187
  const hasValidIcon = isValidElement<LynxIconElementProps>(icon);
167
188
 
@@ -0,0 +1,41 @@
1
+ import "@testing-library/jest-dom";
2
+ import { getQueriesForElement, render } from "@lynx-js/react/testing-library";
3
+ import { describe, expect, it } from "vitest";
4
+
5
+ import { MannerTemp, MannerTempEmote } from "./MannerTemp";
6
+
7
+ function getRenderedRoot() {
8
+ const root = elementTree.root;
9
+
10
+ if (!root) {
11
+ throw new Error("Expected Lynx render root to exist.");
12
+ }
13
+
14
+ return root;
15
+ }
16
+
17
+ describe("MannerTemp", () => {
18
+ it("renders the label and emote with the selected level", () => {
19
+ render(
20
+ <MannerTemp className="custom-manner-temp" level="l6">
21
+ 40°C
22
+ <MannerTempEmote />
23
+ </MannerTemp>,
24
+ );
25
+
26
+ const root = getRenderedRoot();
27
+ const { getByText } = getQueriesForElement(root);
28
+ const mannerTempRoot = root.querySelector(".seed-manner-temp__root");
29
+ const label = getByText("40°C");
30
+ const emote = root.querySelector(".seed-manner-temp__emote");
31
+
32
+ expect(mannerTempRoot).toHaveClass("custom-manner-temp");
33
+ expect(mannerTempRoot).toHaveClass("seed-manner-temp__root--level_l6");
34
+ expect(label).toHaveClass("seed-manner-temp__label");
35
+ expect(label).toHaveClass("seed-manner-temp__label--level_l6");
36
+ expect(emote).toHaveAttribute(
37
+ "src",
38
+ expect.stringContaining("bf8f9b4d-c72e-4bf2-a094-460d3ad1b11f"),
39
+ );
40
+ });
41
+ });
@@ -0,0 +1,85 @@
1
+ import { mannerTemp, type MannerTempVariantProps } from "@seed-design/lynx-css/recipes/manner-temp";
2
+ import type { NodesRef } from "@lynx-js/types";
3
+ import clsx from "clsx";
4
+ import * as React from "@lynx-js/react";
5
+ import { isValidElement } from "@lynx-js/react";
6
+
7
+ import type { LynxStyledElementProps, LynxViewRef } from "../../types";
8
+ import { toArray } from "../../utils/children";
9
+
10
+ type MannerTempLevel = NonNullable<MannerTempVariantProps["level"]>;
11
+
12
+ const emoteSources = {
13
+ l1: "https://assetstorage.krrt.io/1123529253537884138/b63c9b3c-410c-4cf5-ba83-d787a03c3c57/width_96_height_96.webp",
14
+ l2: "https://assetstorage.krrt.io/1123529253537884138/8e4bc458-f6d4-41ce-bfa2-17ea34022f5b/width_96_height_96.webp",
15
+ l3: "https://assetstorage.krrt.io/1123529253537884138/906ff501-edf6-402d-a3a4-a93ce9e04d30/width_96_height_96.webp",
16
+ l4: "https://assetstorage.krrt.io/1123529253537884138/10753976-b25d-4e4c-84b2-dbbe196dd0d9/width_96_height_96.webp",
17
+ l5: "https://assetstorage.krrt.io/1123529253537884138/ddfbd296-5089-408c-aa37-919f45074e5e/width_96_height_96.webp",
18
+ l6: "https://assetstorage.krrt.io/1123529253537884138/bf8f9b4d-c72e-4bf2-a094-460d3ad1b11f/width_96_height_96.webp",
19
+ l7: "https://assetstorage.krrt.io/1123529253537884138/9906a459-a4e3-4521-a6e5-0cc24c0aa763/width_96_height_96.webp",
20
+ l8: "https://assetstorage.krrt.io/1123529253537884138/438edadb-d31d-4711-aebd-a72f303fdf49/width_96_height_96.webp",
21
+ l9: "https://assetstorage.krrt.io/1123529253537884138/61f6c297-11da-4d72-ba90-20cd69c09c22/width_96_height_96.webp",
22
+ l10: "https://assetstorage.krrt.io/1123529253537884138/6cc410ac-3a55-4542-b49f-53551db74c4d/width_96_height_96.webp",
23
+ } satisfies Record<MannerTempLevel, string>;
24
+
25
+ const MannerTempLevelContext = React.createContext<MannerTempLevel | null>(null);
26
+
27
+ export interface MannerTempEmoteProps extends LynxStyledElementProps {
28
+ level?: MannerTempLevel;
29
+ }
30
+
31
+ export const MannerTempEmote = React.forwardRef<unknown, MannerTempEmoteProps>((props, ref) => {
32
+ const contextLevel = React.useContext(MannerTempLevelContext);
33
+ const { level = contextLevel ?? "l1", children: _children, className, ...nativeProps } = props;
34
+ const classes = mannerTemp({ level });
35
+
36
+ return (
37
+ <image
38
+ {...(ref ? { ref: ref as React.Ref<NodesRef> } : {})}
39
+ {...nativeProps}
40
+ src={emoteSources[level]}
41
+ mode="aspectFit"
42
+ accessibility-elements-hidden={true}
43
+ className={clsx(classes.emote, className)}
44
+ />
45
+ );
46
+ });
47
+
48
+ MannerTempEmote.displayName = "MannerTempEmote";
49
+
50
+ function isMannerTempEmote(node: React.ReactNode) {
51
+ return isValidElement(node) && node.type === MannerTempEmote;
52
+ }
53
+
54
+ /**
55
+ * @platform Lynx
56
+ *
57
+ * React와 같은 `level` variant와 `MannerTempEmote` 조합을 제공합니다.
58
+ * HTML `<span>` 대신 native `<view>` / `<text>` / `<image>`를 렌더링합니다.
59
+ */
60
+ export interface MannerTempProps extends MannerTempVariantProps, LynxStyledElementProps {}
61
+
62
+ export const MannerTemp = React.forwardRef<unknown, MannerTempProps>((props, ref) => {
63
+ const [variantProps, otherProps] = mannerTemp.splitVariantProps(props);
64
+ const classes = mannerTemp(variantProps);
65
+ const { children, className, ...nativeProps } = otherProps;
66
+ const childArray = toArray(children);
67
+ const emoteChildren = childArray.filter(isMannerTempEmote);
68
+ const labelChildren = childArray.filter((child) => !isMannerTempEmote(child));
69
+ const level = variantProps.level ?? "l1";
70
+
71
+ return (
72
+ <MannerTempLevelContext.Provider value={level}>
73
+ <view
74
+ {...(ref ? { ref: ref as LynxViewRef } : {})}
75
+ {...nativeProps}
76
+ className={clsx(classes.root, className)}
77
+ >
78
+ <text className={classes.label}>{labelChildren}</text>
79
+ {emoteChildren}
80
+ </view>
81
+ </MannerTempLevelContext.Provider>
82
+ );
83
+ });
84
+
85
+ MannerTemp.displayName = "MannerTemp";
@@ -0,0 +1 @@
1
+ export * from "./MannerTemp";
@@ -0,0 +1,35 @@
1
+ import "@testing-library/jest-dom";
2
+ import { getQueriesForElement, render } from "@lynx-js/react/testing-library";
3
+ import { describe, expect, it } from "vitest";
4
+
5
+ import { MannerTempBadge } from "./MannerTempBadge";
6
+
7
+ function getRenderedRoot() {
8
+ const root = elementTree.root;
9
+
10
+ if (!root) {
11
+ throw new Error("Expected Lynx render root to exist.");
12
+ }
13
+
14
+ return root;
15
+ }
16
+
17
+ describe("MannerTempBadge", () => {
18
+ it("renders the label with the selected level and preserves className", () => {
19
+ render(
20
+ <MannerTempBadge className="custom-manner-temp-badge" level="l10">
21
+ 80°C
22
+ </MannerTempBadge>,
23
+ );
24
+
25
+ const root = getRenderedRoot();
26
+ const { getByText } = getQueriesForElement(root);
27
+ const badgeRoot = root.querySelector(".seed-manner-temp-badge__root");
28
+ const label = getByText("80°C");
29
+
30
+ expect(badgeRoot).toHaveClass("custom-manner-temp-badge");
31
+ expect(badgeRoot).toHaveClass("seed-manner-temp-badge__root--level_l10");
32
+ expect(label).toHaveClass("seed-manner-temp-badge__label");
33
+ expect(label).toHaveClass("seed-manner-temp-badge__label--level_l10");
34
+ });
35
+ });
@@ -0,0 +1,34 @@
1
+ import {
2
+ mannerTempBadge,
3
+ type MannerTempBadgeVariantProps,
4
+ } from "@seed-design/lynx-css/recipes/manner-temp-badge";
5
+ import clsx from "clsx";
6
+ import * as React from "@lynx-js/react";
7
+
8
+ import type { LynxStyledElementProps, LynxViewRef } from "../../types";
9
+
10
+ /**
11
+ * @platform Lynx
12
+ *
13
+ * React와 같은 `level` variant를 제공하지만, HTML `<span>` 대신 native
14
+ * `<view>` / `<text>`를 렌더링합니다.
15
+ */
16
+ export interface MannerTempBadgeProps extends MannerTempBadgeVariantProps, LynxStyledElementProps {}
17
+
18
+ export const MannerTempBadge = React.forwardRef<unknown, MannerTempBadgeProps>((props, ref) => {
19
+ const [variantProps, otherProps] = mannerTempBadge.splitVariantProps(props);
20
+ const classes = mannerTempBadge(variantProps);
21
+ const { children, className, ...nativeProps } = otherProps;
22
+
23
+ return (
24
+ <view
25
+ {...(ref ? { ref: ref as LynxViewRef } : {})}
26
+ {...nativeProps}
27
+ className={clsx(classes.root, className)}
28
+ >
29
+ <text className={classes.label}>{children}</text>
30
+ </view>
31
+ );
32
+ });
33
+
34
+ MannerTempBadge.displayName = "MannerTempBadge";
@@ -0,0 +1 @@
1
+ export * from "./MannerTempBadge";
@@ -0,0 +1,28 @@
1
+ import "@testing-library/jest-dom";
2
+ import { render } from "@lynx-js/react/testing-library";
3
+ import { describe, expect, it } from "vitest";
4
+
5
+ import { NotificationBadge, NotificationBadgePositioner } from "./NotificationBadge";
6
+
7
+ describe("NotificationBadge", () => {
8
+ it("inherits the positioner's size and preserves user class names", () => {
9
+ render(
10
+ <view>
11
+ <NotificationBadgePositioner attach="text" size="small" className="custom-positioner">
12
+ <NotificationBadge className="custom-badge" />
13
+ </NotificationBadgePositioner>
14
+ </view>,
15
+ );
16
+
17
+ const positioner = elementTree.root?.querySelector(".seed-notification-badge-positioner");
18
+ const badge = elementTree.root?.querySelector(".seed-notification-badge__root");
19
+ const label = elementTree.root?.querySelector(".seed-notification-badge__label");
20
+
21
+ expect(positioner).toHaveClass("custom-positioner");
22
+ expect(positioner).toHaveClass("seed-notification-badge-positioner--attach_text");
23
+ expect(positioner).toHaveClass("seed-notification-badge-positioner--size_small");
24
+ expect(badge).toHaveClass("custom-badge");
25
+ expect(badge).toHaveClass("seed-notification-badge__root--size_small");
26
+ expect(label).toHaveAttribute("text-single-line-vertical-align", "center");
27
+ });
28
+ });
@@ -0,0 +1,81 @@
1
+ import {
2
+ notificationBadge,
3
+ type NotificationBadgeVariantProps,
4
+ } from "@seed-design/lynx-css/recipes/notification-badge";
5
+ import {
6
+ notificationBadgePositioner,
7
+ type NotificationBadgePositionerVariantProps,
8
+ } from "@seed-design/lynx-css/recipes/notification-badge-positioner";
9
+ import clsx from "clsx";
10
+ import * as React from "@lynx-js/react";
11
+
12
+ import type {
13
+ LynxAccessibilityProps,
14
+ LynxStyledElementProps,
15
+ LynxViewRef,
16
+ } from "../../types";
17
+
18
+ const NotificationBadgeContext = React.createContext<NotificationBadgeVariantProps | null>(null);
19
+
20
+ /**
21
+ * @platform Lynx
22
+ *
23
+ * 웹 Notification Badge와 동일한 `size` variant를 제공하며, Lynx native `<view>`와
24
+ * `<text>`를 직접 렌더링합니다. `NotificationBadgePositioner` 안에서는 Positioner의 `size`를
25
+ * 기본값으로 사용하고, Badge에 직접 전달한 `size`가 이를 덮어씁니다.
26
+ */
27
+ export interface NotificationBadgeProps
28
+ extends NotificationBadgeVariantProps,
29
+ LynxStyledElementProps,
30
+ LynxAccessibilityProps {}
31
+
32
+ export const NotificationBadge = React.forwardRef<unknown, NotificationBadgeProps>(
33
+ (innerProps, ref) => {
34
+ const contextProps = React.useContext(NotificationBadgeContext);
35
+ const props = { ...contextProps, ...innerProps };
36
+ const [variantProps, otherProps] = notificationBadge.splitVariantProps(props);
37
+ const classes = notificationBadge(variantProps);
38
+ const { children, className, ...nativeProps } = otherProps;
39
+
40
+ return (
41
+ <view
42
+ {...(ref ? { ref: ref as LynxViewRef } : {})}
43
+ {...nativeProps}
44
+ className={clsx(classes.root, className)}
45
+ >
46
+ <text className={classes.label} text-single-line-vertical-align="center">
47
+ {children}
48
+ </text>
49
+ </view>
50
+ );
51
+ },
52
+ );
53
+
54
+ NotificationBadge.displayName = "NotificationBadge";
55
+
56
+ export interface NotificationBadgePositionerProps
57
+ extends NotificationBadgePositionerVariantProps,
58
+ LynxStyledElementProps {}
59
+
60
+ export const NotificationBadgePositioner = React.forwardRef<
61
+ unknown,
62
+ NotificationBadgePositionerProps
63
+ >((props, ref) => {
64
+ const [variantProps, otherProps] = notificationBadgePositioner.splitVariantProps(props);
65
+ const { children, className, ...nativeProps } = otherProps;
66
+ const contextValue = React.useMemo(() => ({ size: variantProps.size }), [variantProps.size]);
67
+
68
+ return (
69
+ <NotificationBadgeContext.Provider value={contextValue}>
70
+ <view
71
+ {...(ref ? { ref: ref as LynxViewRef } : {})}
72
+ {...nativeProps}
73
+ className={clsx(notificationBadgePositioner(variantProps), className)}
74
+ >
75
+ {children}
76
+ </view>
77
+ </NotificationBadgeContext.Provider>
78
+ );
79
+ });
80
+
81
+ NotificationBadgePositioner.displayName = "NotificationBadgePositioner";
@@ -0,0 +1 @@
1
+ export * from "./NotificationBadge";
@@ -0,0 +1,16 @@
1
+ export {
2
+ PageBannerBody as Body,
3
+ PageBannerButton as Button,
4
+ PageBannerCloseButton as CloseButton,
5
+ PageBannerContent as Content,
6
+ PageBannerDescription as Description,
7
+ PageBannerRoot as Root,
8
+ PageBannerTitle as Title,
9
+ type PageBannerBodyProps as BodyProps,
10
+ type PageBannerButtonProps as ButtonProps,
11
+ type PageBannerCloseButtonProps as CloseButtonProps,
12
+ type PageBannerContentProps as ContentProps,
13
+ type PageBannerDescriptionProps as DescriptionProps,
14
+ type PageBannerRootProps as RootProps,
15
+ type PageBannerTitleProps as TitleProps,
16
+ } from "./PageBanner";
@@ -0,0 +1,124 @@
1
+ import "@testing-library/jest-dom";
2
+ import { fireEvent, render } from "@lynx-js/react/testing-library";
3
+ import { describe, expect, it, vi } from "vitest";
4
+
5
+ import * as PageBanner from "./PageBanner.namespace";
6
+
7
+ function getRenderedRoot() {
8
+ const root = elementTree.root;
9
+
10
+ if (!root) {
11
+ throw new Error("Expected Lynx render root to exist.");
12
+ }
13
+
14
+ return root;
15
+ }
16
+
17
+ function getPageBannerRoot() {
18
+ const root = getRenderedRoot();
19
+
20
+ if (root.classList.contains("seed-page-banner__root")) return root;
21
+
22
+ const pageBannerRoot = root.querySelector<HTMLElement>(".seed-page-banner__root");
23
+ if (!pageBannerRoot) {
24
+ throw new Error("Expected PageBanner root to exist.");
25
+ }
26
+
27
+ return pageBannerRoot;
28
+ }
29
+
30
+ describe("PageBanner", () => {
31
+ it("renders compound slots with the default neutral weak style", () => {
32
+ render(
33
+ <PageBanner.Root>
34
+ <PageBanner.Content>
35
+ <PageBanner.Body>
36
+ <PageBanner.Title>Title</PageBanner.Title>
37
+ <PageBanner.Description>Description</PageBanner.Description>
38
+ </PageBanner.Body>
39
+ <PageBanner.Button>Action</PageBanner.Button>
40
+ </PageBanner.Content>
41
+ <PageBanner.CloseButton accessibility-label="Close" />
42
+ </PageBanner.Root>,
43
+ );
44
+
45
+ const root = getPageBannerRoot();
46
+
47
+ expect(root).toHaveClass("seed-page-banner__root--tone_neutral-variant_weak");
48
+ expect(root.querySelector(".seed-page-banner__content")?.tagName.toLowerCase()).toBe("view");
49
+ expect(root.querySelector(".seed-page-banner__body")?.tagName.toLowerCase()).toBe("text");
50
+ expect(root.querySelector(".seed-page-banner__title")).toHaveClass(
51
+ "seed-page-banner__title--tone_neutral-variant_weak",
52
+ );
53
+ expect(root.querySelector(".seed-page-banner__description")).toHaveClass(
54
+ "seed-page-banner__description--tone_neutral-variant_weak",
55
+ );
56
+ expect(root.querySelector(".seed-page-banner__button")).toHaveAttribute(
57
+ "accessibility-traits",
58
+ "button",
59
+ );
60
+ });
61
+
62
+ it("tracks pressed state and maps actionable accessibility", () => {
63
+ const onTap = vi.fn();
64
+
65
+ render(
66
+ <PageBanner.Root tone="positive" variant="solid" bindtap={onTap}>
67
+ <PageBanner.Content>
68
+ <PageBanner.Body>
69
+ <PageBanner.Description>Actionable</PageBanner.Description>
70
+ </PageBanner.Body>
71
+ </PageBanner.Content>
72
+ </PageBanner.Root>,
73
+ );
74
+
75
+ const root = getPageBannerRoot();
76
+
77
+ expect(root).toHaveAttribute("accessibility-element", "true");
78
+ expect(root).toHaveAttribute("accessibility-traits", "button");
79
+
80
+ fireEvent.touchstart(root, {});
81
+ expect(root).toHaveClass("seed-page-banner__root--tone_positive-variant_solid-pressed_true");
82
+
83
+ fireEvent.tap(root);
84
+ expect(onTap).toHaveBeenCalledTimes(1);
85
+ });
86
+
87
+ it("dismisses an uncontrolled banner and composes the close tap handler", () => {
88
+ const onTap = vi.fn();
89
+ const onDismiss = vi.fn();
90
+
91
+ render(
92
+ <PageBanner.Root onDismiss={onDismiss}>
93
+ <PageBanner.CloseButton bindtap={onTap} accessibility-label="Close" />
94
+ </PageBanner.Root>,
95
+ );
96
+
97
+ const closeButton = getPageBannerRoot().querySelector(".seed-page-banner__closeButton");
98
+ if (!closeButton) throw new Error("Expected close button to exist.");
99
+
100
+ fireEvent.tap(closeButton);
101
+
102
+ expect(onTap).toHaveBeenCalledTimes(1);
103
+ expect(onDismiss).toHaveBeenCalledTimes(1);
104
+ expect(getRenderedRoot().querySelector(".seed-page-banner__root")).not.toBeInTheDocument();
105
+ });
106
+
107
+ it("notifies without hiding a controlled banner", () => {
108
+ const onDismiss = vi.fn();
109
+
110
+ render(
111
+ <PageBanner.Root open onDismiss={onDismiss}>
112
+ <PageBanner.CloseButton accessibility-label="Close" />
113
+ </PageBanner.Root>,
114
+ );
115
+
116
+ const closeButton = getPageBannerRoot().querySelector(".seed-page-banner__closeButton");
117
+ if (!closeButton) throw new Error("Expected close button to exist.");
118
+
119
+ fireEvent.tap(closeButton);
120
+
121
+ expect(onDismiss).toHaveBeenCalledTimes(1);
122
+ expect(getPageBannerRoot()).toBeInTheDocument();
123
+ });
124
+ });