From 028ee39c981fd330f252e9ae2822b0d0cbc77d93 Mon Sep 17 00:00:00 2001 From: Minseo Kim Date: Mon, 9 Mar 2026 21:16:07 +0000 Subject: [PATCH 1/2] Initial react ui component --- packages/lytenyte-ui-react/LICENSE | 13 + packages/lytenyte-ui-react/package.json | 47 +++ .../src/accordion/accordion-context.tsx | 12 + .../src/accordion/accordion-item-context.tsx | 12 + .../src/accordion/accordion-item.tsx | 20 ++ .../src/accordion/accordion-root.tsx | 335 ++++++++++++++++++ .../src/hooks/merge-props.ts | 22 ++ .../src/hooks/use-class-name.ts | 6 + .../src/hooks/use-combined-ref.ts | 16 + .../src/hooks/use-controlled.ts | 25 ++ .../lytenyte-ui-react/src/hooks/use-slot.ts | 26 ++ .../lytenyte-ui-react/src/hooks/use-style.ts | 6 + .../src/hooks/use-transition-status.ts | 84 +++++ packages/lytenyte-ui-react/src/index.ts | 1 + packages/lytenyte-ui-react/src/type.ts | 9 + packages/lytenyte-ui-react/tsconfig.json | 29 ++ pnpm-lock.yaml | 12 + 17 files changed, 675 insertions(+) create mode 100644 packages/lytenyte-ui-react/LICENSE create mode 100644 packages/lytenyte-ui-react/package.json create mode 100644 packages/lytenyte-ui-react/src/accordion/accordion-context.tsx create mode 100644 packages/lytenyte-ui-react/src/accordion/accordion-item-context.tsx create mode 100644 packages/lytenyte-ui-react/src/accordion/accordion-item.tsx create mode 100644 packages/lytenyte-ui-react/src/accordion/accordion-root.tsx create mode 100644 packages/lytenyte-ui-react/src/hooks/merge-props.ts create mode 100644 packages/lytenyte-ui-react/src/hooks/use-class-name.ts create mode 100644 packages/lytenyte-ui-react/src/hooks/use-combined-ref.ts create mode 100644 packages/lytenyte-ui-react/src/hooks/use-controlled.ts create mode 100644 packages/lytenyte-ui-react/src/hooks/use-slot.ts create mode 100644 packages/lytenyte-ui-react/src/hooks/use-style.ts create mode 100644 packages/lytenyte-ui-react/src/hooks/use-transition-status.ts create mode 100644 packages/lytenyte-ui-react/src/index.ts create mode 100644 packages/lytenyte-ui-react/src/type.ts create mode 100644 packages/lytenyte-ui-react/tsconfig.json diff --git a/packages/lytenyte-ui-react/LICENSE b/packages/lytenyte-ui-react/LICENSE new file mode 100644 index 000000000..52a87aa62 --- /dev/null +++ b/packages/lytenyte-ui-react/LICENSE @@ -0,0 +1,13 @@ +Copyright 2025 1771 Technologies + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/packages/lytenyte-ui-react/package.json b/packages/lytenyte-ui-react/package.json new file mode 100644 index 000000000..f2e273df4 --- /dev/null +++ b/packages/lytenyte-ui-react/package.json @@ -0,0 +1,47 @@ +{ + "name": "@1771technologies/lytenyte-ui-react", + "description": "LyteNyte React component library", + "license": "Apache-2.0", + "keywords": [ + "TODO" + ], + "version": "0.0.1", + "type": "module", + "files": [ + "dist", + "LICENSE" + ], + "homepage": "https://1771technologies.com", + "repository": { + "type": "git", + "url": "https://github.com/1771-technologies/lytenyte.git", + "directory": "packages/lytenyte-ui-react" + }, + "bugs": { + "url": "https://github.com/1771-technologies/lytenyte/issues" + }, + "exports": { + ".": { + "types": "./src/index.ts", + "import": "./src/index.ts", + "require": "./src/index.ts" + } + }, + "publishConfig": { + "access": "public", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.js" + } + } + }, + "dependencies": { + "@1771technologies/lytenyte-design": "workspace:*" + }, + "peerDependencies": { + "react": "18.0.0 || ^19.0.0", + "react-dom": "18.0.0 || ^19.0.0" + } +} diff --git a/packages/lytenyte-ui-react/src/accordion/accordion-context.tsx b/packages/lytenyte-ui-react/src/accordion/accordion-context.tsx new file mode 100644 index 000000000..e6efeaccc --- /dev/null +++ b/packages/lytenyte-ui-react/src/accordion/accordion-context.tsx @@ -0,0 +1,12 @@ +import { createContext, useContext } from "react"; + +export interface AccordionContextValue { + readonly collapsible: boolean; + readonly hiddenUntilFound: boolean; + readonly disabled: boolean; +} + +const context = createContext({} as AccordionContextValue); + +export const AccordionRootProvider = context.Provider; +export const useAccordionRoot = () => useContext(context); diff --git a/packages/lytenyte-ui-react/src/accordion/accordion-item-context.tsx b/packages/lytenyte-ui-react/src/accordion/accordion-item-context.tsx new file mode 100644 index 000000000..b45145ed8 --- /dev/null +++ b/packages/lytenyte-ui-react/src/accordion/accordion-item-context.tsx @@ -0,0 +1,12 @@ +import { createContext, useContext } from "react"; + +export interface AccordionItemContext { + readonly collapsible: boolean; + readonly collapsed: boolean; + readonly hiddenUntilFound: boolean; + readonly disabled: boolean; +} + +const context = createContext({} as AccordionItemContext); +export const AccordionItemProvider = context.Provider; +export const useAccordionItem = () => useContext(context); diff --git a/packages/lytenyte-ui-react/src/accordion/accordion-item.tsx b/packages/lytenyte-ui-react/src/accordion/accordion-item.tsx new file mode 100644 index 000000000..bf7adf9f2 --- /dev/null +++ b/packages/lytenyte-ui-react/src/accordion/accordion-item.tsx @@ -0,0 +1,20 @@ +import { forwardRef, type ReactElement } from "react"; +import type { Accordion } from "./accordion-root"; +import { useSlot } from "../hooks/use-slot.js"; + +function AccordionItemBase( + { value, onOpenChange, disabled, style, className, render, ...props }: Accordion.Item.Props, + ref: Accordion.Item.Props["ref"], +) { + const slot = useSlot({ + slot: render ??
, + props: [props], + ref: ref, + }); + + return slot; +} + +export const AccordionItem = forwardRef(AccordionItemBase) as ( + props: Accordion.Item.Props, +) => ReactElement; diff --git a/packages/lytenyte-ui-react/src/accordion/accordion-root.tsx b/packages/lytenyte-ui-react/src/accordion/accordion-root.tsx new file mode 100644 index 000000000..95780d934 --- /dev/null +++ b/packages/lytenyte-ui-react/src/accordion/accordion-root.tsx @@ -0,0 +1,335 @@ +import type { JSX } from "react"; +import { forwardRef, useMemo } from "react"; +import type { EventWithDetails, ClassNameWithState, SlotComponent, StyleWithState } from "../type.js"; +import { useSlot } from "../hooks/use-slot.js"; +import { useControlled } from "../hooks/use-controlled.js"; +import type { AccordionContextValue } from "./accordion-context.js"; +import { AccordionRootProvider } from "./accordion-context.js"; +import { useClassName } from "../hooks/use-class-name.js"; +import { useStyle } from "../hooks/use-style.js"; + +function AccordionImpl( + { + render, + collapsible, + value: providedValue, + hiddenUntilFound, + disabled: providedDisabled, + // multiple, + // loopFocus, + orientation: providedOrientation, + style: providedStyle, + className: providedClassName, + // keepMounted, + // onValueChange, + ...props + }: Accordion.Props, + forwarded: Accordion.Props["ref"], +) { + const [value, _setValue] = useControlled({ controlled: providedValue, default: [] }); + const disabled = providedDisabled ?? false; + const orientation = providedOrientation ?? "vertical"; + + const state = useMemo(() => { + return { value, disabled, orientation }; + }, [value, disabled, orientation]); + + const className = useClassName(providedClassName, state); + const style = useStyle(providedStyle, state); + + const slot = useSlot>({ + slot: render ??
, + props: [{ className, style }, props], + ref: forwarded, + state: state, + }); + + const contextValue = useMemo(() => { + return { + collapsible: collapsible ?? false, + disabled: disabled, + hiddenUntilFound: hiddenUntilFound ?? false, + }; + }, [collapsible, disabled, hiddenUntilFound]); + + return {slot}; +} + +export const Accordion = forwardRef(AccordionImpl); + +export namespace Accordion { + export type AccordionOrientation = "vertical" | "horizontal"; + export interface State { + readonly value: T[]; + readonly disabled: boolean; + readonly orientation: AccordionOrientation; + } + + export interface ChangeEventDetails {} + export interface FocusChangeEventDetails {} + + export type Props = Omit & { + /** + * Override the default element that is rendered by the accordion root. + * The slot element provided must accept a valid ref attribute. + * + * Accepts a `ReactElement` or a function that + * will given the accordion state and returns a `ReactElement` + * + * @default
+ */ + readonly render?: SlotComponent>; + + /** + * Whether an accordion item can be closed after it has been expanded. Setting the + * `collapsible` property on the root level changes the default for all accordion items. + * + * @default false + */ + readonly collapsible?: boolean; + + /** + * The initial value of the expanded accordion items. Use when you want to set the initial + * state of the accordion but do not want to control state updates. + */ + readonly defaultValue?: T[]; + + /** + * The controlled value of the accordion. If this value is set, then the `defaultValue` property + * will be ignored. Use this property when you want to have direct control over the accordion state. + * + * Set the `onValueChange` property to handle accordion expansion changes. + */ + readonly value?: T[]; + + /** + * Allows the browser's built-in page search to find and expand the panel contents. Overrides + * the `keepMounted` prop and uses the `hidden="until-found"` property to hide the element without + * removing it from the DOM. + * + * To learn more about the `hidden` property see + * [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/hidden) + * + * @default false + */ + readonly hiddenUntilFound?: boolean; + + /** + * Whether the accordion items can receive user interaction. Setting disabled on the Accordion root will apply + * to all items, however individual items may override the setting by setting their own + * disable state. + * + * @default false + */ + readonly disabled?: boolean; + + /** + * Whether multiple items can be open at the same time. + * + * If the `value` property is set with multiple items, whilst `multiple` is `false`, the accordion + * will respect the `value` property and display multiple open accordions. However, subsequent + * expansion changes will collapse/expand accordions to ensure only a single item is open. + * + * For the most predictable result only use `multiple` if your application logic can handle + * `value` states that contain more than a single item. + * + * @default false + */ + readonly multiple?: boolean; + + /** + * Whether to cycle through accordion items when using the arrow keys to navigate the accordion. + * + * @default true + */ + readonly loopFocus?: boolean; + + /** + * The visual orientation of the accordion. Controls whether the roving focus uses left/right + * or up/down arrow keys. + * + * @default "vertical" + */ + readonly orientation?: "horizontal" | "vertical"; + + /** + * Inline styles applied to the root accordion element, or a function that returns inline styled + * based on the Accordion's state. + */ + readonly style?: StyleWithState; + + /** + * CSS classes applied to the root accordion element, or a function that returns classes based + * on the Accordion's state. + */ + readonly className?: ClassNameWithState; + + /** + * Whether to mount the element in the DOM while the panel is closed. This property is ignored + * when `hiddenUntilFound` is set to `true`. + */ + readonly keepMounted?: boolean; + + /** + * Event handler called when an accordion item is expanded or collapsed. Provides the new + * accordion value state when called. + */ + readonly onValueChange?: EventWithDetails; + }; + + export namespace Item { + export interface State {} + export interface OpenChangeEventDetails {} + + export type Props = Omit & { + /** + * The value associated with this accordion item. The value should be unique among all + * the accordion items. If the value is present in the Root's `value` the accordion will be + * expanded. + */ + readonly value?: T; + + /** + * Event handler called when the open state of this accordion item changes. Provides + * the new open state as a boolean when called. + */ + readonly onOpenChange?: EventWithDetails; + + /** + * Whether the accordion item can receive user interaction. Overrides the `disabled` + * setting from the accordion root for this specific item. + * + * @default false + */ + readonly disabled?: boolean; + + /** + * Inline styles applied to the item element, or a function that returns inline styles + * based on the item's state. + */ + readonly style?: StyleWithState; + + /** + * CSS classes applied to the item element, or a function that returns classes based + * on the item's state. + */ + readonly className?: ClassNameWithState; + + /** + * Override the default element that is rendered by the accordion item. + * The slot element provided must accept a valid ref attribute. + * + * Accepts a `ReactElement` or a function that + * will given the item state and returns a `ReactElement`. + * + * @default
+ */ + readonly render: SlotComponent; + }; + } + + export namespace Header { + export interface State {} + + export type Props = JSX.IntrinsicElements["h3"] & { + /** + * Inline styles applied to the header element, or a function that returns inline styles + * based on the header's state. + */ + readonly style?: StyleWithState; + + /** + * CSS classes applied to the header element, or a function that returns classes based + * on the header's state. + */ + readonly className?: ClassNameWithState; + + /** + * Override the default element that is rendered by the accordion header. + * The slot element provided must accept a valid ref attribute. + * + * Accepts a `ReactElement` or a function that + * will given the header state and returns a `ReactElement`. + * + * @default

+ */ + readonly render: SlotComponent; + }; + } + + export namespace Trigger { + export interface State {} + + export type Props = JSX.IntrinsicElements["button"] & { + /** + * Inline styles applied to the trigger element, or a function that returns inline styles + * based on the trigger's state. + */ + readonly style?: StyleWithState; + + /** + * CSS classes applied to the trigger element, or a function that returns classes based + * on the trigger's state. + */ + readonly className?: ClassNameWithState; + + /** + * Override the default element that is rendered by the accordion trigger. + * The slot element provided must accept a valid ref attribute. + * + * Accepts a `ReactElement` or a function that + * will given the trigger state and returns a `ReactElement`. + * + * @default