(
)
);
-NitroIcon.displayName = 'NitroIcon';
\ No newline at end of file
+NitroIcon.displayName = 'NitroIcon';
diff --git a/packages/nitro-react/src/components/NitroTooltip.tsx b/packages/nitro-react/src/components/NitroTooltip.tsx
new file mode 100644
index 0000000..74a412d
--- /dev/null
+++ b/packages/nitro-react/src/components/NitroTooltip.tsx
@@ -0,0 +1,324 @@
+import { type ReactNode, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
+import { createPortal } from 'react-dom';
+
+import { cn, extractNumber } from '#base/utils';
+
+export type NitroTooltipFollowType = boolean | 'x' | 'y';
+
+export type NitroTooltipPlacementType = 'top' | 'top-left' | 'top-right' | 'left' | 'right' | 'bottom' | 'bottom-left' | 'bottom-right';
+
+type TooltipSideType = 'top' | 'bottom' | 'left' | 'right';
+
+type TooltipAlignType = 'left' | 'right';
+
+interface TooltipBounds {
+ left: number;
+ right: number;
+ top: number;
+ bottom: number;
+}
+
+interface TooltipSize {
+ width: number;
+ height: number;
+}
+
+export const DEFAULT_TOOLTIP_ID = 'nitro-tooltip';
+
+const PLACEMENTS: NitroTooltipPlacementType[] = ['top', 'top-left', 'top-right', 'left', 'right', 'bottom', 'bottom-left', 'bottom-right'];
+
+export interface NitroTooltipRenderProps {
+ anchor: HTMLElement;
+ content: string | null;
+}
+
+export interface NitroTooltipProps {
+ id?: string;
+ follow?: NitroTooltipFollowType;
+ placement?: NitroTooltipPlacementType;
+ delay?: number;
+ offset?: number;
+ className?: string;
+ container?: Element | null;
+ render?: (props: NitroTooltipRenderProps) => ReactNode;
+}
+
+export const NitroTooltip = ({ id = DEFAULT_TOOLTIP_ID, follow = true, placement = 'bottom-left', delay = 0, offset = 12, className, container, render }: NitroTooltipProps) => {
+ const VIEWPORT_MARGIN = 1;
+ const VERIFY_INTERVAL = 150;
+ const CURSOR_RADIUS = 3;
+
+ const [anchor, setAnchor] = useState
(null);
+ const [content, setContent] = useState(null);
+ const [pinned, setPinned] = useState(false);
+
+ const timerRef = useRef(0);
+ const pointRef = useRef({ x: 0, y: 0 });
+ const tooltipRef = useRef(null);
+
+ const selector = `[data-tooltip-id="${id}"]`;
+
+ const close = useCallback(() => {
+ window.clearTimeout(timerRef.current);
+
+ setAnchor(null);
+ setContent(null);
+ setPinned(false);
+ }, []);
+
+ const show = useCallback((element: HTMLElement) => {
+ setAnchor(element);
+ setContent(element.dataset.tooltipContent ?? null);
+ }, []);
+
+ const anchorAt = useCallback((x: number, y: number) => {
+ const element = document.elementFromPoint(x, y);
+
+ return (element instanceof Element) ? element.closest(selector) : null;
+ }, [selector]);
+
+ const resolveFollowProperty = (value: string | undefined, fallback: NitroTooltipFollowType): NitroTooltipFollowType => {
+ if ((value === 'x') || (value === 'y')) return value;
+ if (value === 'true') return true;
+ if (value === 'false') return false;
+
+ return fallback;
+ };
+
+ const resolvePlacementProperty = (value: string | undefined, fallback: NitroTooltipPlacementType): NitroTooltipPlacementType => {
+ return PLACEMENTS.includes(value as NitroTooltipPlacementType) ? value as NitroTooltipPlacementType : fallback;
+ }
+
+ const clamp = (value: number, size: number, viewport: number) => {
+ return Math.min(Math.max(VIEWPORT_MARGIN, value), viewport - size - VIEWPORT_MARGIN);
+ }
+
+ const calculateElementPosition = (placement: NitroTooltipPlacementType, reference: TooltipBounds, size: TooltipSize, offset: number, clearance: { x: number, y: number }) => {
+ const [side, align] = placement.split('-') as [TooltipSideType, TooltipAlignType?];
+
+ const beforeX = reference.left - clearance.x - size.width - offset;
+ const afterX = reference.right + clearance.x + offset;
+ const beforeY = reference.top - clearance.y - size.height - offset;
+ const afterY = reference.bottom + clearance.y + offset;
+
+ if ((side === 'left') || (side === 'right')) {
+ const x = (side === 'left') ? beforeX : afterX;
+
+ const fits = (side === 'left')
+ ? (x >= VIEWPORT_MARGIN)
+ : ((x + size.width) <= (window.innerWidth - VIEWPORT_MARGIN));
+
+ return {
+ x: fits ? x : ((side === 'left') ? afterX : beforeX),
+ y: (reference.top + reference.bottom - size.height) / 2
+ };
+ }
+
+ const y = (side === 'top') ? beforeY : afterY;
+
+ const fits = (side === 'top')
+ ? (y >= VIEWPORT_MARGIN)
+ : ((y + size.height) <= (window.innerHeight - VIEWPORT_MARGIN));
+
+ let alignedX = reference.left + clearance.x;
+
+ if(align !== 'left') {
+ alignedX = align === 'right'
+ ? reference.right - clearance.x - size.width
+ : (reference.left + reference.right - size.width) / 2;
+ }
+
+ return {
+ x: alignedX,
+ y: fits ? y : ((side === 'top') ? afterY : beforeY)
+ };
+ };
+
+ const place = useCallback(() => {
+ const node = tooltipRef.current;
+
+ if (!anchor || !node) return;
+
+ const rect = anchor.getBoundingClientRect();
+ const size = node.getBoundingClientRect();
+
+ const point = pointRef.current;
+ const activeFollow = resolveFollowProperty(anchor.dataset.tooltipFollow, follow);
+ const activePlacement = resolvePlacementProperty(anchor.dataset.tooltipPlacement, placement);
+
+ const followX = !pinned && ((activeFollow === true) || (activeFollow === 'x'));
+ const followY = !pinned && ((activeFollow === true) || (activeFollow === 'y'));
+
+ const reference: TooltipBounds = {
+ left: followX ? point.x : rect.left,
+ right: followX ? point.x : rect.right,
+ top: followY ? point.y : rect.top,
+ bottom: followY ? point.y : rect.bottom
+ };
+
+ const clearance = {
+ x: followX ? CURSOR_RADIUS : 0,
+ y: followY ? CURSOR_RADIUS : 0
+ };
+
+ const { x, y } = calculateElementPosition(activePlacement, reference, size, offset, clearance);
+
+ node.style.transform = `translate3d(${Math.round(clamp(x, size.width, window.innerWidth))}px, ${Math.round(clamp(y, size.height, window.innerHeight))}px, 0)`;
+ }, [anchor, pinned, follow, placement, offset]);
+
+ useEffect(() => {
+ const onPointerOver = (event: PointerEvent) => {
+ if (pinned) return;
+
+ const target = event.target;
+ const element = (target instanceof Element) ? target.closest(selector) : null;
+
+ if (!element || (element === anchor)) return;
+
+ pointRef.current = { x: event.clientX, y: event.clientY };
+
+ window.clearTimeout(timerRef.current);
+
+ const wait = anchor ? 0 : extractNumber(element.dataset.tooltipDelay, delay);
+
+ if (wait <= 0) {
+ show(element);
+ return;
+ }
+
+ timerRef.current = window.setTimeout(() => {
+ if (anchorAt(pointRef.current.x, pointRef.current.y) !== element) return;
+
+ show(element);
+ }, wait);
+ };
+
+ document.addEventListener('pointerover', onPointerOver, true);
+
+ return () => document.removeEventListener('pointerover', onPointerOver, true);
+ }, [selector, anchor, pinned, delay, show, anchorAt]);
+
+ useEffect(() => {
+ const onPointerDown = (event: PointerEvent) => {
+ const target = event.target;
+ const element = (target instanceof Element) ? target.closest(selector) : null;
+
+ if (pinned) {
+ if (tooltipRef.current?.contains(target as Node)) return;
+ if (element && (element !== anchor)) return;
+
+ close();
+
+ return;
+ }
+
+ if (!element || (element.dataset.tooltipFixed == null)) return;
+
+ pointRef.current = { x: event.clientX, y: event.clientY };
+
+ window.clearTimeout(timerRef.current);
+
+ show(element);
+ setPinned(true);
+ };
+
+ const onKeyDown = (event: KeyboardEvent) => {
+ if (event.key === 'Escape') close();
+ };
+
+ document.addEventListener('pointerdown', onPointerDown, true);
+ document.addEventListener('keydown', onKeyDown);
+
+ return () => {
+ document.removeEventListener('pointerdown', onPointerDown, true);
+ document.removeEventListener('keydown', onKeyDown);
+ };
+ }, [selector, anchor, pinned, close, show]);
+
+ useEffect(() => {
+ const track = (event: PointerEvent) => {
+ pointRef.current = { x: event.clientX, y: event.clientY };
+ };
+
+ document.addEventListener('pointermove', track, true);
+
+ return () => document.removeEventListener('pointermove', track, true);
+ }, []);
+
+ useLayoutEffect(() => {
+ if (anchor) place();
+ }, [anchor, content, place]);
+
+ useEffect(() => {
+ if (!anchor) return;
+
+ const update = () => {
+ if (!anchor.isConnected) return close();
+
+ const rect = anchor.getBoundingClientRect();
+
+ if (!rect.width || !rect.height) return close();
+
+ if (!pinned) {
+ const { x, y } = pointRef.current;
+ const found = anchorAt(x, y);
+
+ if (found !== anchor) {
+ if (found) show(found);
+ else close();
+
+ return;
+ }
+
+ const next = anchor.dataset.tooltipContent ?? null;
+
+ setContent(previous => (previous === next) ? previous : next);
+ }
+
+ place();
+ };
+
+ const onVisibility = () => {
+ if (document.hidden) close();
+ };
+
+ const interval = window.setInterval(update, VERIFY_INTERVAL);
+
+ document.addEventListener('pointermove', update, true);
+ document.documentElement.addEventListener('pointerleave', close);
+ document.addEventListener('visibilitychange', onVisibility);
+
+ window.addEventListener('scroll', update, true);
+ window.addEventListener('resize', update);
+ window.addEventListener('blur', close);
+
+ return () => {
+ window.clearInterval(interval);
+
+ document.removeEventListener('pointermove', update, true);
+ document.documentElement.removeEventListener('pointerleave', close);
+ document.removeEventListener('visibilitychange', onVisibility);
+
+ window.removeEventListener('scroll', update, true);
+ window.removeEventListener('resize', update);
+ window.removeEventListener('blur', close);
+ };
+ }, [anchor, pinned, place, anchorAt, close, show]);
+
+ useEffect(() => () => window.clearTimeout(timerRef.current), []);
+
+ const target = container ?? document.getElementById('ui-container') ?? document.body;
+ const body = anchor ? (render ? render({ anchor, content }) : content) : null;
+
+ if (!anchor || !target || (body == null) || (body === '')) return null;
+
+ const tooltip = (
+
+ {body}
+
+ );
+
+ return createPortal(tooltip, target);
+};
diff --git a/packages/nitro-react/src/components/index.ts b/packages/nitro-react/src/components/index.ts
index d37d4bb..32432f7 100644
--- a/packages/nitro-react/src/components/index.ts
+++ b/packages/nitro-react/src/components/index.ts
@@ -1,7 +1,13 @@
+export * from './AutoGrid';
export * from './AvatarImage';
+export * from './Base';
export * from './Button';
+export * from './Column';
+export * from './Flex';
export * from './FurnitureImage';
+export * from './Grid';
export * from './NitroIcon';
+export * from './NitroTooltip';
export * from './room/RoomCanvas';
export * from './room/RoomContainer';
export * from './room/RoomEventHandler';
diff --git a/packages/nitro-react/src/index.css b/packages/nitro-react/src/index.css
index 0228e3d..bc6bd4a 100644
--- a/packages/nitro-react/src/index.css
+++ b/packages/nitro-react/src/index.css
@@ -4,4 +4,5 @@
@import './theme.css';
@import './assets/fonts.css';
@import './assets/utilities.css';
+@import './assets/components.css';
@import './assets/icons.css';
diff --git a/packages/nitro-react/src/utils/styles/AlignContentType.ts b/packages/nitro-react/src/utils/styles/AlignContentType.ts
new file mode 100644
index 0000000..79e4b02
--- /dev/null
+++ b/packages/nitro-react/src/utils/styles/AlignContentType.ts
@@ -0,0 +1,12 @@
+export const AlignContentType = {
+ start: 'content-start',
+ end: 'content-end',
+ center: 'content-center',
+ between: 'content-between',
+ around: 'content-around',
+ evenly: 'content-evenly',
+ stretch: 'content-stretch',
+ baseline: 'content-baseline'
+};
+
+export type AlignContentType = keyof typeof AlignContentType;
diff --git a/packages/nitro-react/src/utils/styles/AlignItemsType.ts b/packages/nitro-react/src/utils/styles/AlignItemsType.ts
new file mode 100644
index 0000000..9a99aff
--- /dev/null
+++ b/packages/nitro-react/src/utils/styles/AlignItemsType.ts
@@ -0,0 +1,9 @@
+export const AlignItemsType = {
+ start: 'items-start',
+ end: 'items-end',
+ center: 'items-center',
+ baseline: 'items-baseline',
+ stretch: 'items-stretch'
+};
+
+export type AlignItemsType = keyof typeof AlignItemsType;
diff --git a/packages/nitro-react/src/utils/styles/AlignSelfType.ts b/packages/nitro-react/src/utils/styles/AlignSelfType.ts
new file mode 100644
index 0000000..3c278fe
--- /dev/null
+++ b/packages/nitro-react/src/utils/styles/AlignSelfType.ts
@@ -0,0 +1,10 @@
+export const AlignSelfType = {
+ auto: 'self-auto',
+ start: 'self-start',
+ end: 'self-end',
+ center: 'self-center',
+ baseline: 'self-baseline',
+ stretch: 'self-stretch'
+};
+
+export type AlignSelfType = keyof typeof AlignSelfType;
diff --git a/packages/nitro-react/src/utils/styles/CursorType.ts b/packages/nitro-react/src/utils/styles/CursorType.ts
new file mode 100644
index 0000000..3129b3b
--- /dev/null
+++ b/packages/nitro-react/src/utils/styles/CursorType.ts
@@ -0,0 +1,15 @@
+export const CursorType = {
+ auto: 'cursor-auto',
+ default: 'cursor-default',
+ pointer: 'cursor-pointer',
+ text: 'cursor-text',
+ move: 'cursor-move',
+ grab: 'cursor-grab',
+ grabbing: 'cursor-grabbing',
+ wait: 'cursor-wait',
+ help: 'cursor-help',
+ notAllowed: 'cursor-not-allowed',
+ none: 'cursor-none'
+};
+
+export type CursorType = keyof typeof CursorType;
diff --git a/packages/nitro-react/src/utils/styles/DisplayType.ts b/packages/nitro-react/src/utils/styles/DisplayType.ts
new file mode 100644
index 0000000..46bc388
--- /dev/null
+++ b/packages/nitro-react/src/utils/styles/DisplayType.ts
@@ -0,0 +1,13 @@
+export const DisplayType = {
+ block: 'block',
+ inlineBlock: 'inline-block',
+ inline: 'inline',
+ flex: 'flex',
+ inlineFlex: 'inline-flex',
+ grid: 'grid',
+ inlineGrid: 'inline-grid',
+ contents: 'contents',
+ none: 'hidden'
+};
+
+export type DisplayType = keyof typeof DisplayType;
diff --git a/packages/nitro-react/src/utils/styles/FlexDirectionType.ts b/packages/nitro-react/src/utils/styles/FlexDirectionType.ts
new file mode 100644
index 0000000..f20a7f9
--- /dev/null
+++ b/packages/nitro-react/src/utils/styles/FlexDirectionType.ts
@@ -0,0 +1,8 @@
+export const FlexDirectionType = {
+ row: 'flex-row',
+ rowReverse: 'flex-row-reverse',
+ col: 'flex-col',
+ colReverse: 'flex-col-reverse'
+};
+
+export type FlexDirectionType = keyof typeof FlexDirectionType;
diff --git a/packages/nitro-react/src/utils/styles/FlexGrowType.ts b/packages/nitro-react/src/utils/styles/FlexGrowType.ts
new file mode 100644
index 0000000..43157aa
--- /dev/null
+++ b/packages/nitro-react/src/utils/styles/FlexGrowType.ts
@@ -0,0 +1,6 @@
+export const FlexGrowType = {
+ 0: 'grow-0',
+ 1: 'grow'
+};
+
+export type FlexGrowType = keyof typeof FlexGrowType;
diff --git a/packages/nitro-react/src/utils/styles/FlexShrinkType.ts b/packages/nitro-react/src/utils/styles/FlexShrinkType.ts
new file mode 100644
index 0000000..eafc1c4
--- /dev/null
+++ b/packages/nitro-react/src/utils/styles/FlexShrinkType.ts
@@ -0,0 +1,6 @@
+export const FlexShrinkType = {
+ 0: 'shrink-0',
+ 1: 'shrink'
+};
+
+export type FlexShrinkType = keyof typeof FlexShrinkType;
diff --git a/packages/nitro-react/src/utils/styles/FlexType.ts b/packages/nitro-react/src/utils/styles/FlexType.ts
new file mode 100644
index 0000000..c24b448
--- /dev/null
+++ b/packages/nitro-react/src/utils/styles/FlexType.ts
@@ -0,0 +1,8 @@
+export const FlexType = {
+ 1: 'flex-1',
+ auto: 'flex-auto',
+ initial: 'flex-initial',
+ none: 'flex-none'
+};
+
+export type FlexType = keyof typeof FlexType;
diff --git a/packages/nitro-react/src/utils/styles/FlexWrapType.ts b/packages/nitro-react/src/utils/styles/FlexWrapType.ts
new file mode 100644
index 0000000..edae7bb
--- /dev/null
+++ b/packages/nitro-react/src/utils/styles/FlexWrapType.ts
@@ -0,0 +1,7 @@
+export const FlexWrapType = {
+ wrap: 'flex-wrap',
+ reverse: 'flex-wrap-reverse',
+ no: 'flex-nowrap'
+};
+
+export type FlexWrapType = keyof typeof FlexWrapType;
diff --git a/packages/nitro-react/src/utils/styles/GapType.ts b/packages/nitro-react/src/utils/styles/GapType.ts
new file mode 100644
index 0000000..9c50c82
--- /dev/null
+++ b/packages/nitro-react/src/utils/styles/GapType.ts
@@ -0,0 +1,52 @@
+export const GapType = {
+ all: {
+ 0: 'gap-0',
+ 1: 'gap-1',
+ 2: 'gap-2',
+ 3: 'gap-3',
+ 4: 'gap-4',
+ 5: 'gap-5',
+ 6: 'gap-6',
+ 7: 'gap-7',
+ 8: 'gap-8',
+ 10: 'gap-10',
+ 12: 'gap-12',
+ 16: 'gap-16',
+ 20: 'gap-20',
+ 24: 'gap-24'
+ },
+ x: {
+ 0: 'gap-x-0',
+ 1: 'gap-x-1',
+ 2: 'gap-x-2',
+ 3: 'gap-x-3',
+ 4: 'gap-x-4',
+ 5: 'gap-x-5',
+ 6: 'gap-x-6',
+ 7: 'gap-x-7',
+ 8: 'gap-x-8',
+ 10: 'gap-x-10',
+ 12: 'gap-x-12',
+ 16: 'gap-x-16',
+ 20: 'gap-x-20',
+ 24: 'gap-x-24'
+ },
+ y: {
+ 0: 'gap-y-0',
+ 1: 'gap-y-1',
+ 2: 'gap-y-2',
+ 3: 'gap-y-3',
+ 4: 'gap-y-4',
+ 5: 'gap-y-5',
+ 6: 'gap-y-6',
+ 7: 'gap-y-7',
+ 8: 'gap-y-8',
+ 10: 'gap-y-10',
+ 12: 'gap-y-12',
+ 16: 'gap-y-16',
+ 20: 'gap-y-20',
+ 24: 'gap-y-24'
+ }
+};
+
+export type GapType = keyof typeof GapType.all;
diff --git a/packages/nitro-react/src/utils/styles/GridColSpanType.ts b/packages/nitro-react/src/utils/styles/GridColSpanType.ts
new file mode 100644
index 0000000..e26f58c
--- /dev/null
+++ b/packages/nitro-react/src/utils/styles/GridColSpanType.ts
@@ -0,0 +1,18 @@
+export const GridColSpanType = {
+ 1: 'col-span-1',
+ 2: 'col-span-2',
+ 3: 'col-span-3',
+ 4: 'col-span-4',
+ 5: 'col-span-5',
+ 6: 'col-span-6',
+ 7: 'col-span-7',
+ 8: 'col-span-8',
+ 9: 'col-span-9',
+ 10: 'col-span-10',
+ 11: 'col-span-11',
+ 12: 'col-span-12',
+ full: 'col-span-full',
+ auto: 'col-auto'
+};
+
+export type GridColSpanType = keyof typeof GridColSpanType;
diff --git a/packages/nitro-react/src/utils/styles/GridColStartType.ts b/packages/nitro-react/src/utils/styles/GridColStartType.ts
new file mode 100644
index 0000000..f886b4d
--- /dev/null
+++ b/packages/nitro-react/src/utils/styles/GridColStartType.ts
@@ -0,0 +1,17 @@
+export const GridColStartType = {
+ 1: 'col-start-1',
+ 2: 'col-start-2',
+ 3: 'col-start-3',
+ 4: 'col-start-4',
+ 5: 'col-start-5',
+ 6: 'col-start-6',
+ 7: 'col-start-7',
+ 8: 'col-start-8',
+ 9: 'col-start-9',
+ 10: 'col-start-10',
+ 11: 'col-start-11',
+ 12: 'col-start-12',
+ auto: 'col-start-auto'
+};
+
+export type GridColStartType = keyof typeof GridColStartType;
diff --git a/packages/nitro-react/src/utils/styles/GridColumnsType.ts b/packages/nitro-react/src/utils/styles/GridColumnsType.ts
new file mode 100644
index 0000000..a7ccd1b
--- /dev/null
+++ b/packages/nitro-react/src/utils/styles/GridColumnsType.ts
@@ -0,0 +1,18 @@
+export const GridColumnsType = {
+ 1: 'grid-cols-1',
+ 2: 'grid-cols-2',
+ 3: 'grid-cols-3',
+ 4: 'grid-cols-4',
+ 5: 'grid-cols-5',
+ 6: 'grid-cols-6',
+ 7: 'grid-cols-7',
+ 8: 'grid-cols-8',
+ 9: 'grid-cols-9',
+ 10: 'grid-cols-10',
+ 11: 'grid-cols-11',
+ 12: 'grid-cols-12',
+ none: 'grid-cols-none',
+ subgrid: 'grid-cols-subgrid'
+};
+
+export type GridColumnsType = keyof typeof GridColumnsType;
diff --git a/packages/nitro-react/src/utils/styles/GridFlowType.ts b/packages/nitro-react/src/utils/styles/GridFlowType.ts
new file mode 100644
index 0000000..2ee18c8
--- /dev/null
+++ b/packages/nitro-react/src/utils/styles/GridFlowType.ts
@@ -0,0 +1,9 @@
+export const GridFlowType = {
+ row: 'grid-flow-row',
+ col: 'grid-flow-col',
+ dense: 'grid-flow-dense',
+ rowDense: 'grid-flow-row-dense',
+ colDense: 'grid-flow-col-dense'
+};
+
+export type GridFlowType = keyof typeof GridFlowType;
diff --git a/packages/nitro-react/src/utils/styles/GridRowSpanType.ts b/packages/nitro-react/src/utils/styles/GridRowSpanType.ts
new file mode 100644
index 0000000..7aef5ca
--- /dev/null
+++ b/packages/nitro-react/src/utils/styles/GridRowSpanType.ts
@@ -0,0 +1,18 @@
+export const GridRowSpanType = {
+ 1: 'row-span-1',
+ 2: 'row-span-2',
+ 3: 'row-span-3',
+ 4: 'row-span-4',
+ 5: 'row-span-5',
+ 6: 'row-span-6',
+ 7: 'row-span-7',
+ 8: 'row-span-8',
+ 9: 'row-span-9',
+ 10: 'row-span-10',
+ 11: 'row-span-11',
+ 12: 'row-span-12',
+ full: 'row-span-full',
+ auto: 'row-auto'
+};
+
+export type GridRowSpanType = keyof typeof GridRowSpanType;
diff --git a/packages/nitro-react/src/utils/styles/GridRowsType.ts b/packages/nitro-react/src/utils/styles/GridRowsType.ts
new file mode 100644
index 0000000..c6b1317
--- /dev/null
+++ b/packages/nitro-react/src/utils/styles/GridRowsType.ts
@@ -0,0 +1,18 @@
+export const GridRowsType = {
+ 1: 'grid-rows-1',
+ 2: 'grid-rows-2',
+ 3: 'grid-rows-3',
+ 4: 'grid-rows-4',
+ 5: 'grid-rows-5',
+ 6: 'grid-rows-6',
+ 7: 'grid-rows-7',
+ 8: 'grid-rows-8',
+ 9: 'grid-rows-9',
+ 10: 'grid-rows-10',
+ 11: 'grid-rows-11',
+ 12: 'grid-rows-12',
+ none: 'grid-rows-none',
+ subgrid: 'grid-rows-subgrid'
+};
+
+export type GridRowsType = keyof typeof GridRowsType;
diff --git a/packages/nitro-react/src/utils/styles/JustifyContentType.ts b/packages/nitro-react/src/utils/styles/JustifyContentType.ts
new file mode 100644
index 0000000..8946476
--- /dev/null
+++ b/packages/nitro-react/src/utils/styles/JustifyContentType.ts
@@ -0,0 +1,12 @@
+export const JustifyContentType = {
+ start: 'justify-start',
+ end: 'justify-end',
+ center: 'justify-center',
+ between: 'justify-between',
+ around: 'justify-around',
+ evenly: 'justify-evenly',
+ stretch: 'justify-stretch',
+ normal: 'justify-normal'
+};
+
+export type JustifyContentType = keyof typeof JustifyContentType;
diff --git a/packages/nitro-react/src/utils/styles/JustifyItemsType.ts b/packages/nitro-react/src/utils/styles/JustifyItemsType.ts
new file mode 100644
index 0000000..11000e5
--- /dev/null
+++ b/packages/nitro-react/src/utils/styles/JustifyItemsType.ts
@@ -0,0 +1,9 @@
+export const JustifyItemsType = {
+ start: 'justify-items-start',
+ end: 'justify-items-end',
+ center: 'justify-items-center',
+ stretch: 'justify-items-stretch',
+ normal: 'justify-items-normal'
+};
+
+export type JustifyItemsType = keyof typeof JustifyItemsType;
diff --git a/packages/nitro-react/src/utils/styles/JustifySelfType.ts b/packages/nitro-react/src/utils/styles/JustifySelfType.ts
new file mode 100644
index 0000000..b035687
--- /dev/null
+++ b/packages/nitro-react/src/utils/styles/JustifySelfType.ts
@@ -0,0 +1,9 @@
+export const JustifySelfType = {
+ auto: 'justify-self-auto',
+ start: 'justify-self-start',
+ end: 'justify-self-end',
+ center: 'justify-self-center',
+ stretch: 'justify-self-stretch'
+};
+
+export type JustifySelfType = keyof typeof JustifySelfType;
diff --git a/packages/nitro-react/src/utils/styles/OverflowType.ts b/packages/nitro-react/src/utils/styles/OverflowType.ts
new file mode 100644
index 0000000..55c34fe
--- /dev/null
+++ b/packages/nitro-react/src/utils/styles/OverflowType.ts
@@ -0,0 +1,22 @@
+export const OverflowType = {
+ all: {
+ auto: 'overflow-auto',
+ hidden: 'overflow-hidden',
+ visible: 'overflow-visible',
+ scroll: 'overflow-scroll'
+ },
+ x: {
+ auto: 'overflow-x-auto',
+ hidden: 'overflow-x-hidden',
+ visible: 'overflow-x-visible',
+ scroll: 'overflow-x-scroll'
+ },
+ y: {
+ auto: 'overflow-y-auto',
+ hidden: 'overflow-y-hidden',
+ visible: 'overflow-y-visible',
+ scroll: 'overflow-y-scroll'
+ }
+};
+
+export type OverflowType = keyof typeof OverflowType.all;
diff --git a/packages/nitro-react/src/utils/styles/PositionType.ts b/packages/nitro-react/src/utils/styles/PositionType.ts
new file mode 100644
index 0000000..f90c0c5
--- /dev/null
+++ b/packages/nitro-react/src/utils/styles/PositionType.ts
@@ -0,0 +1,9 @@
+export const PositionType = {
+ static: 'static',
+ relative: 'relative',
+ absolute: 'absolute',
+ fixed: 'fixed',
+ sticky: 'sticky'
+};
+
+export type PositionType = keyof typeof PositionType;
diff --git a/packages/nitro-react/src/utils/styles/TextColorType.ts b/packages/nitro-react/src/utils/styles/TextColorType.ts
new file mode 100644
index 0000000..82a87e1
--- /dev/null
+++ b/packages/nitro-react/src/utils/styles/TextColorType.ts
@@ -0,0 +1,295 @@
+export const TextColorType = {
+ inherit: 'text-inherit',
+ current: 'text-current',
+ transparent: 'text-transparent',
+ black: 'text-black',
+ white: 'text-white',
+ 'slate-50': 'text-slate-50',
+ 'slate-100': 'text-slate-100',
+ 'slate-200': 'text-slate-200',
+ 'slate-300': 'text-slate-300',
+ 'slate-400': 'text-slate-400',
+ 'slate-500': 'text-slate-500',
+ 'slate-600': 'text-slate-600',
+ 'slate-700': 'text-slate-700',
+ 'slate-800': 'text-slate-800',
+ 'slate-900': 'text-slate-900',
+ 'slate-950': 'text-slate-950',
+ 'gray-50': 'text-gray-50',
+ 'gray-100': 'text-gray-100',
+ 'gray-200': 'text-gray-200',
+ 'gray-300': 'text-gray-300',
+ 'gray-400': 'text-gray-400',
+ 'gray-500': 'text-gray-500',
+ 'gray-600': 'text-gray-600',
+ 'gray-700': 'text-gray-700',
+ 'gray-800': 'text-gray-800',
+ 'gray-900': 'text-gray-900',
+ 'gray-950': 'text-gray-950',
+ 'zinc-50': 'text-zinc-50',
+ 'zinc-100': 'text-zinc-100',
+ 'zinc-200': 'text-zinc-200',
+ 'zinc-300': 'text-zinc-300',
+ 'zinc-400': 'text-zinc-400',
+ 'zinc-500': 'text-zinc-500',
+ 'zinc-600': 'text-zinc-600',
+ 'zinc-700': 'text-zinc-700',
+ 'zinc-800': 'text-zinc-800',
+ 'zinc-900': 'text-zinc-900',
+ 'zinc-950': 'text-zinc-950',
+ 'neutral-50': 'text-neutral-50',
+ 'neutral-100': 'text-neutral-100',
+ 'neutral-200': 'text-neutral-200',
+ 'neutral-300': 'text-neutral-300',
+ 'neutral-400': 'text-neutral-400',
+ 'neutral-500': 'text-neutral-500',
+ 'neutral-600': 'text-neutral-600',
+ 'neutral-700': 'text-neutral-700',
+ 'neutral-800': 'text-neutral-800',
+ 'neutral-900': 'text-neutral-900',
+ 'neutral-950': 'text-neutral-950',
+ 'stone-50': 'text-stone-50',
+ 'stone-100': 'text-stone-100',
+ 'stone-200': 'text-stone-200',
+ 'stone-300': 'text-stone-300',
+ 'stone-400': 'text-stone-400',
+ 'stone-500': 'text-stone-500',
+ 'stone-600': 'text-stone-600',
+ 'stone-700': 'text-stone-700',
+ 'stone-800': 'text-stone-800',
+ 'stone-900': 'text-stone-900',
+ 'stone-950': 'text-stone-950',
+ 'mauve-50': 'text-mauve-50',
+ 'mauve-100': 'text-mauve-100',
+ 'mauve-200': 'text-mauve-200',
+ 'mauve-300': 'text-mauve-300',
+ 'mauve-400': 'text-mauve-400',
+ 'mauve-500': 'text-mauve-500',
+ 'mauve-600': 'text-mauve-600',
+ 'mauve-700': 'text-mauve-700',
+ 'mauve-800': 'text-mauve-800',
+ 'mauve-900': 'text-mauve-900',
+ 'mauve-950': 'text-mauve-950',
+ 'olive-50': 'text-olive-50',
+ 'olive-100': 'text-olive-100',
+ 'olive-200': 'text-olive-200',
+ 'olive-300': 'text-olive-300',
+ 'olive-400': 'text-olive-400',
+ 'olive-500': 'text-olive-500',
+ 'olive-600': 'text-olive-600',
+ 'olive-700': 'text-olive-700',
+ 'olive-800': 'text-olive-800',
+ 'olive-900': 'text-olive-900',
+ 'olive-950': 'text-olive-950',
+ 'mist-50': 'text-mist-50',
+ 'mist-100': 'text-mist-100',
+ 'mist-200': 'text-mist-200',
+ 'mist-300': 'text-mist-300',
+ 'mist-400': 'text-mist-400',
+ 'mist-500': 'text-mist-500',
+ 'mist-600': 'text-mist-600',
+ 'mist-700': 'text-mist-700',
+ 'mist-800': 'text-mist-800',
+ 'mist-900': 'text-mist-900',
+ 'mist-950': 'text-mist-950',
+ 'taupe-50': 'text-taupe-50',
+ 'taupe-100': 'text-taupe-100',
+ 'taupe-200': 'text-taupe-200',
+ 'taupe-300': 'text-taupe-300',
+ 'taupe-400': 'text-taupe-400',
+ 'taupe-500': 'text-taupe-500',
+ 'taupe-600': 'text-taupe-600',
+ 'taupe-700': 'text-taupe-700',
+ 'taupe-800': 'text-taupe-800',
+ 'taupe-900': 'text-taupe-900',
+ 'taupe-950': 'text-taupe-950',
+ 'red-50': 'text-red-50',
+ 'red-100': 'text-red-100',
+ 'red-200': 'text-red-200',
+ 'red-300': 'text-red-300',
+ 'red-400': 'text-red-400',
+ 'red-500': 'text-red-500',
+ 'red-600': 'text-red-600',
+ 'red-700': 'text-red-700',
+ 'red-800': 'text-red-800',
+ 'red-900': 'text-red-900',
+ 'red-950': 'text-red-950',
+ 'orange-50': 'text-orange-50',
+ 'orange-100': 'text-orange-100',
+ 'orange-200': 'text-orange-200',
+ 'orange-300': 'text-orange-300',
+ 'orange-400': 'text-orange-400',
+ 'orange-500': 'text-orange-500',
+ 'orange-600': 'text-orange-600',
+ 'orange-700': 'text-orange-700',
+ 'orange-800': 'text-orange-800',
+ 'orange-900': 'text-orange-900',
+ 'orange-950': 'text-orange-950',
+ 'amber-50': 'text-amber-50',
+ 'amber-100': 'text-amber-100',
+ 'amber-200': 'text-amber-200',
+ 'amber-300': 'text-amber-300',
+ 'amber-400': 'text-amber-400',
+ 'amber-500': 'text-amber-500',
+ 'amber-600': 'text-amber-600',
+ 'amber-700': 'text-amber-700',
+ 'amber-800': 'text-amber-800',
+ 'amber-900': 'text-amber-900',
+ 'amber-950': 'text-amber-950',
+ 'yellow-50': 'text-yellow-50',
+ 'yellow-100': 'text-yellow-100',
+ 'yellow-200': 'text-yellow-200',
+ 'yellow-300': 'text-yellow-300',
+ 'yellow-400': 'text-yellow-400',
+ 'yellow-500': 'text-yellow-500',
+ 'yellow-600': 'text-yellow-600',
+ 'yellow-700': 'text-yellow-700',
+ 'yellow-800': 'text-yellow-800',
+ 'yellow-900': 'text-yellow-900',
+ 'yellow-950': 'text-yellow-950',
+ 'lime-50': 'text-lime-50',
+ 'lime-100': 'text-lime-100',
+ 'lime-200': 'text-lime-200',
+ 'lime-300': 'text-lime-300',
+ 'lime-400': 'text-lime-400',
+ 'lime-500': 'text-lime-500',
+ 'lime-600': 'text-lime-600',
+ 'lime-700': 'text-lime-700',
+ 'lime-800': 'text-lime-800',
+ 'lime-900': 'text-lime-900',
+ 'lime-950': 'text-lime-950',
+ 'green-50': 'text-green-50',
+ 'green-100': 'text-green-100',
+ 'green-200': 'text-green-200',
+ 'green-300': 'text-green-300',
+ 'green-400': 'text-green-400',
+ 'green-500': 'text-green-500',
+ 'green-600': 'text-green-600',
+ 'green-700': 'text-green-700',
+ 'green-800': 'text-green-800',
+ 'green-900': 'text-green-900',
+ 'green-950': 'text-green-950',
+ 'emerald-50': 'text-emerald-50',
+ 'emerald-100': 'text-emerald-100',
+ 'emerald-200': 'text-emerald-200',
+ 'emerald-300': 'text-emerald-300',
+ 'emerald-400': 'text-emerald-400',
+ 'emerald-500': 'text-emerald-500',
+ 'emerald-600': 'text-emerald-600',
+ 'emerald-700': 'text-emerald-700',
+ 'emerald-800': 'text-emerald-800',
+ 'emerald-900': 'text-emerald-900',
+ 'emerald-950': 'text-emerald-950',
+ 'teal-50': 'text-teal-50',
+ 'teal-100': 'text-teal-100',
+ 'teal-200': 'text-teal-200',
+ 'teal-300': 'text-teal-300',
+ 'teal-400': 'text-teal-400',
+ 'teal-500': 'text-teal-500',
+ 'teal-600': 'text-teal-600',
+ 'teal-700': 'text-teal-700',
+ 'teal-800': 'text-teal-800',
+ 'teal-900': 'text-teal-900',
+ 'teal-950': 'text-teal-950',
+ 'cyan-50': 'text-cyan-50',
+ 'cyan-100': 'text-cyan-100',
+ 'cyan-200': 'text-cyan-200',
+ 'cyan-300': 'text-cyan-300',
+ 'cyan-400': 'text-cyan-400',
+ 'cyan-500': 'text-cyan-500',
+ 'cyan-600': 'text-cyan-600',
+ 'cyan-700': 'text-cyan-700',
+ 'cyan-800': 'text-cyan-800',
+ 'cyan-900': 'text-cyan-900',
+ 'cyan-950': 'text-cyan-950',
+ 'sky-50': 'text-sky-50',
+ 'sky-100': 'text-sky-100',
+ 'sky-200': 'text-sky-200',
+ 'sky-300': 'text-sky-300',
+ 'sky-400': 'text-sky-400',
+ 'sky-500': 'text-sky-500',
+ 'sky-600': 'text-sky-600',
+ 'sky-700': 'text-sky-700',
+ 'sky-800': 'text-sky-800',
+ 'sky-900': 'text-sky-900',
+ 'sky-950': 'text-sky-950',
+ 'blue-50': 'text-blue-50',
+ 'blue-100': 'text-blue-100',
+ 'blue-200': 'text-blue-200',
+ 'blue-300': 'text-blue-300',
+ 'blue-400': 'text-blue-400',
+ 'blue-500': 'text-blue-500',
+ 'blue-600': 'text-blue-600',
+ 'blue-700': 'text-blue-700',
+ 'blue-800': 'text-blue-800',
+ 'blue-900': 'text-blue-900',
+ 'blue-950': 'text-blue-950',
+ 'indigo-50': 'text-indigo-50',
+ 'indigo-100': 'text-indigo-100',
+ 'indigo-200': 'text-indigo-200',
+ 'indigo-300': 'text-indigo-300',
+ 'indigo-400': 'text-indigo-400',
+ 'indigo-500': 'text-indigo-500',
+ 'indigo-600': 'text-indigo-600',
+ 'indigo-700': 'text-indigo-700',
+ 'indigo-800': 'text-indigo-800',
+ 'indigo-900': 'text-indigo-900',
+ 'indigo-950': 'text-indigo-950',
+ 'violet-50': 'text-violet-50',
+ 'violet-100': 'text-violet-100',
+ 'violet-200': 'text-violet-200',
+ 'violet-300': 'text-violet-300',
+ 'violet-400': 'text-violet-400',
+ 'violet-500': 'text-violet-500',
+ 'violet-600': 'text-violet-600',
+ 'violet-700': 'text-violet-700',
+ 'violet-800': 'text-violet-800',
+ 'violet-900': 'text-violet-900',
+ 'violet-950': 'text-violet-950',
+ 'purple-50': 'text-purple-50',
+ 'purple-100': 'text-purple-100',
+ 'purple-200': 'text-purple-200',
+ 'purple-300': 'text-purple-300',
+ 'purple-400': 'text-purple-400',
+ 'purple-500': 'text-purple-500',
+ 'purple-600': 'text-purple-600',
+ 'purple-700': 'text-purple-700',
+ 'purple-800': 'text-purple-800',
+ 'purple-900': 'text-purple-900',
+ 'purple-950': 'text-purple-950',
+ 'fuchsia-50': 'text-fuchsia-50',
+ 'fuchsia-100': 'text-fuchsia-100',
+ 'fuchsia-200': 'text-fuchsia-200',
+ 'fuchsia-300': 'text-fuchsia-300',
+ 'fuchsia-400': 'text-fuchsia-400',
+ 'fuchsia-500': 'text-fuchsia-500',
+ 'fuchsia-600': 'text-fuchsia-600',
+ 'fuchsia-700': 'text-fuchsia-700',
+ 'fuchsia-800': 'text-fuchsia-800',
+ 'fuchsia-900': 'text-fuchsia-900',
+ 'fuchsia-950': 'text-fuchsia-950',
+ 'pink-50': 'text-pink-50',
+ 'pink-100': 'text-pink-100',
+ 'pink-200': 'text-pink-200',
+ 'pink-300': 'text-pink-300',
+ 'pink-400': 'text-pink-400',
+ 'pink-500': 'text-pink-500',
+ 'pink-600': 'text-pink-600',
+ 'pink-700': 'text-pink-700',
+ 'pink-800': 'text-pink-800',
+ 'pink-900': 'text-pink-900',
+ 'pink-950': 'text-pink-950',
+ 'rose-50': 'text-rose-50',
+ 'rose-100': 'text-rose-100',
+ 'rose-200': 'text-rose-200',
+ 'rose-300': 'text-rose-300',
+ 'rose-400': 'text-rose-400',
+ 'rose-500': 'text-rose-500',
+ 'rose-600': 'text-rose-600',
+ 'rose-700': 'text-rose-700',
+ 'rose-800': 'text-rose-800',
+ 'rose-900': 'text-rose-900',
+ 'rose-950': 'text-rose-950'
+};
+
+export type TextColorType = keyof typeof TextColorType;
diff --git a/packages/nitro-react/src/utils/styles/index.ts b/packages/nitro-react/src/utils/styles/index.ts
new file mode 100644
index 0000000..b58999c
--- /dev/null
+++ b/packages/nitro-react/src/utils/styles/index.ts
@@ -0,0 +1,23 @@
+export * from './AlignContentType';
+export * from './AlignItemsType';
+export * from './AlignSelfType';
+export * from './CursorType';
+export * from './DisplayType';
+export * from './FlexDirectionType';
+export * from './FlexGrowType';
+export * from './FlexShrinkType';
+export * from './FlexType';
+export * from './FlexWrapType';
+export * from './GapType';
+export * from './GridColSpanType';
+export * from './GridColStartType';
+export * from './GridColumnsType';
+export * from './GridFlowType';
+export * from './GridRowSpanType';
+export * from './GridRowsType';
+export * from './JustifyContentType';
+export * from './JustifyItemsType';
+export * from './JustifySelfType';
+export * from './OverflowType';
+export * from './PositionType';
+export * from './TextColorType';
diff --git a/packages/nitro-react/src/utils/utils.ts b/packages/nitro-react/src/utils/utils.ts
index 6eb81f6..fcd307c 100644
--- a/packages/nitro-react/src/utils/utils.ts
+++ b/packages/nitro-react/src/utils/utils.ts
@@ -55,4 +55,12 @@ export function cva>>(
};
}
+export function extractNumber(value: number | string | undefined, fallback: number): number {
+ if (value == null) return fallback;
+
+ const parsed = Number(value);
+
+ return Number.isFinite(parsed) ? parsed : fallback;
+}
+
export type { VariantProps };
\ No newline at end of file
diff --git a/packages/nitro-react/src/views/room-widgets/object-infostand/InfostandUserView.tsx b/packages/nitro-react/src/views/room-widgets/object-infostand/InfostandUserView.tsx
index a94b48a..55c6b07 100644
--- a/packages/nitro-react/src/views/room-widgets/object-infostand/InfostandUserView.tsx
+++ b/packages/nitro-react/src/views/room-widgets/object-infostand/InfostandUserView.tsx
@@ -69,7 +69,7 @@ export const InfostandUserView = (props: InfostandUserViewProps) => {
{!userData.isOwnUser &&
{motto.length === 0 ? getLocalizationValue('infostand.motto.change') : motto}
}
{userData.isOwnUser && <>
-
setIsEditingMotto(true)} />
+ setIsEditingMotto(true)} />
{!isEditingMotto && {motto}
}
{isEditingMotto && setMotto(e.target.value)} onKeyDown={onMottoKeyDown} autoFocus={true} />}
>}
diff --git a/packages/nitro-react/src/views/room-widgets/object-menu/InfoBubbleAvatarView.tsx b/packages/nitro-react/src/views/room-widgets/object-menu/InfoBubbleAvatarView.tsx
index b57acd0..5f43a5f 100644
--- a/packages/nitro-react/src/views/room-widgets/object-menu/InfoBubbleAvatarView.tsx
+++ b/packages/nitro-react/src/views/room-widgets/object-menu/InfoBubbleAvatarView.tsx
@@ -269,7 +269,7 @@ export const InfoBubbleAvatarView = (props: InfoBubbleAvatarViewProps) => {
>}
setCollapsed(!collapsed)}>
-
+
);
diff --git a/packages/nitro-react/src/views/room-widgets/object-menu/InfoBubbleOwnAvatarView.tsx b/packages/nitro-react/src/views/room-widgets/object-menu/InfoBubbleOwnAvatarView.tsx
index 8944c54..d89e280 100644
--- a/packages/nitro-react/src/views/room-widgets/object-menu/InfoBubbleOwnAvatarView.tsx
+++ b/packages/nitro-react/src/views/room-widgets/object-menu/InfoBubbleOwnAvatarView.tsx
@@ -207,7 +207,7 @@ export const InfoBubbleOwnAvatarView = (props: InfoBubbleOwnAvatarViewProps) =>
>}