@lattice-ui/tabs 0.4.3 → 0.5.0-next.1

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.
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "tabs",
3
+ "tree": {
4
+ "$path": "out",
5
+ "out": {
6
+ "$path": "out"
7
+ }
8
+ }
9
+ }
@@ -1,3 +1,3 @@
1
1
  import { React } from "@lattice-ui/core";
2
2
  import type { TabsContentProps } from "./types";
3
- export declare function TabsContent(props: TabsContentProps): React.JSX.Element | undefined;
3
+ export declare function TabsContent(props: TabsContentProps): React.JSX.Element;
@@ -4,10 +4,23 @@ local _core = TS.import(script, TS.getModule(script, "@lattice-ui", "core").out)
4
4
  local React = _core.React
5
5
  local Slot = _core.Slot
6
6
  local Presence = TS.import(script, TS.getModule(script, "@lattice-ui", "layer").out).Presence
7
+ local _motion = TS.import(script, TS.getModule(script, "@lattice-ui", "motion").out)
8
+ local createSurfaceRevealRecipe = _motion.createSurfaceRevealRecipe
9
+ local usePresenceMotion = _motion.usePresenceMotion
7
10
  local useTabsContext = TS.import(script, script.Parent, "context").useTabsContext
8
11
  local createTabsContentName = TS.import(script, script.Parent, "internals", "ids").createTabsContentName
9
12
  local function TabsContentImpl(props)
10
13
  local contentName = createTabsContentName(props.value)
14
+ local defaultTransition = React.useMemo(function()
15
+ return createSurfaceRevealRecipe()
16
+ end, {})
17
+ local config = React.useMemo(function()
18
+ if not props.transition then
19
+ return defaultTransition
20
+ end
21
+ return props.transition
22
+ end, { defaultTransition, props.transition })
23
+ local motionRef = usePresenceMotion(props.motionPresent, config, props.onExitComplete)
11
24
  if props.asChild then
12
25
  local child = props.children
13
26
  if not React.isValidElement(child) then
@@ -16,6 +29,7 @@ local function TabsContentImpl(props)
16
29
  return React.createElement(Slot, {
17
30
  Name = contentName,
18
31
  Visible = props.visible,
32
+ ref = motionRef,
19
33
  }, child)
20
34
  end
21
35
  return React.createElement("frame", {
@@ -23,30 +37,32 @@ local function TabsContentImpl(props)
23
37
  BorderSizePixel = 0,
24
38
  Size = UDim2.fromOffset(0, 0),
25
39
  Visible = props.visible,
40
+ ref = motionRef,
26
41
  }, props.children)
27
42
  end
28
43
  local function TabsContent(props)
29
44
  local tabsContext = useTabsContext()
30
45
  local selected = tabsContext.value == props.value
31
46
  local forceMount = props.forceMount == true
32
- if not selected and not forceMount then
33
- return nil
34
- end
35
47
  if forceMount then
36
48
  return React.createElement(TabsContentImpl, {
37
49
  asChild = props.asChild,
50
+ motionPresent = selected,
51
+ transition = props.transition,
38
52
  value = props.value,
39
53
  visible = selected,
40
54
  }, props.children)
41
55
  end
42
56
  return React.createElement(Presence, {
43
- exitFallbackMs = 0,
44
57
  present = selected,
45
58
  render = function(state)
46
59
  return React.createElement(TabsContentImpl, {
47
60
  asChild = props.asChild,
61
+ motionPresent = state.isPresent,
62
+ onExitComplete = state.onExitComplete,
63
+ transition = props.transition,
48
64
  value = props.value,
49
- visible = state.isPresent,
65
+ visible = true,
50
66
  }, props.children)
51
67
  end,
52
68
  })
@@ -1,12 +1,13 @@
1
1
  -- Compiled with roblox-ts v3.0.0
2
2
  local TS = _G[script]
3
3
  local _core = TS.import(script, TS.getModule(script, "@lattice-ui", "core").out)
4
- local findOrderedSelectionEntry = _core.findOrderedSelectionEntry
5
- local focusOrderedSelectionEntry = _core.focusOrderedSelectionEntry
6
- local getOrderedSelectionEntries = _core.getOrderedSelectionEntries
7
- local getRelativeOrderedSelectionEntry = _core.getRelativeOrderedSelectionEntry
8
4
  local React = _core.React
9
5
  local useControllableState = _core.useControllableState
6
+ local _focus = TS.import(script, TS.getModule(script, "@lattice-ui", "focus").out)
7
+ local findOrderedSelectionEntry = _focus.findOrderedSelectionEntry
8
+ local focusOrderedSelectionEntry = _focus.focusOrderedSelectionEntry
9
+ local getOrderedSelectionEntries = _focus.getOrderedSelectionEntries
10
+ local getRelativeOrderedSelectionEntry = _focus.getRelativeOrderedSelectionEntry
10
11
  local TabsContextProvider = TS.import(script, script.Parent, "context").TabsContextProvider
11
12
  local function resolveNextValue(currentValue, orderedTriggers, fallbackOrder)
12
13
  -- ▼ ReadonlyArray.filter ▼
@@ -3,7 +3,10 @@ local TS = _G[script]
3
3
  local _core = TS.import(script, TS.getModule(script, "@lattice-ui", "core").out)
4
4
  local React = _core.React
5
5
  local Slot = _core.Slot
6
- local useFocusNode = _core.useFocusNode
6
+ local useFocusNode = TS.import(script, TS.getModule(script, "@lattice-ui", "focus").out).useFocusNode
7
+ local _motion = TS.import(script, TS.getModule(script, "@lattice-ui", "motion").out)
8
+ local createSelectionResponseRecipe = _motion.createSelectionResponseRecipe
9
+ local useResponseMotion = _motion.useResponseMotion
7
10
  local useTabsContext = TS.import(script, script.Parent, "context").useTabsContext
8
11
  local createTabsTriggerName = TS.import(script, script.Parent, "internals", "ids").createTabsTriggerName
9
12
  local nextTriggerId = 0
@@ -50,9 +53,21 @@ local function TabsTrigger(props)
50
53
  return disabledRef.current
51
54
  end,
52
55
  })
56
+ local motionRef = useResponseMotion(selected, {
57
+ active = {
58
+ BackgroundColor3 = Color3.fromRGB(86, 137, 245),
59
+ TextColor3 = Color3.fromRGB(235, 240, 248),
60
+ },
61
+ inactive = {
62
+ BackgroundColor3 = Color3.fromRGB(47, 53, 68),
63
+ TextColor3 = Color3.fromRGB(136, 144, 159),
64
+ },
65
+ }, createSelectionResponseRecipe())
53
66
  local setTriggerRef = React.useCallback(function(instance)
54
- triggerRef.current = toGuiObject(instance)
55
- end, {})
67
+ local nextTrigger = toGuiObject(instance)
68
+ triggerRef.current = nextTrigger
69
+ motionRef.current = nextTrigger
70
+ end, { motionRef })
56
71
  local handleActivated = React.useCallback(function()
57
72
  if disabled then
58
73
  return nil
@@ -112,7 +127,7 @@ local function TabsTrigger(props)
112
127
  Selectable = not disabled,
113
128
  Size = UDim2.fromOffset(132, 34),
114
129
  Text = props.value,
115
- TextColor3 = if disabled then Color3.fromRGB(136, 144, 159) else Color3.fromRGB(235, 240, 248),
130
+ TextColor3 = if selected then Color3.fromRGB(235, 240, 248) elseif disabled then Color3.fromRGB(136, 144, 159) else Color3.fromRGB(235, 240, 248),
116
131
  TextSize = 15,
117
132
  ref = setTriggerRef,
118
133
  }, props.children)
@@ -1,3 +1,4 @@
1
+ import type { PresenceMotionConfig } from "@lattice-ui/motion";
1
2
  import type React from "@rbxts/react";
2
3
  export type TabsSetValue = (value: string) => void;
3
4
  export type TabsOrientation = "horizontal" | "vertical";
@@ -33,6 +34,7 @@ export type TabsTriggerProps = {
33
34
  children?: React.ReactElement;
34
35
  };
35
36
  export type TabsContentProps = {
37
+ transition?: PresenceMotionConfig;
36
38
  value: string;
37
39
  asChild?: boolean;
38
40
  forceMount?: boolean;
package/package.json CHANGED
@@ -1,16 +1,25 @@
1
1
  {
2
2
  "name": "@lattice-ui/tabs",
3
- "version": "0.4.3",
3
+ "version": "0.5.0-next.1",
4
4
  "private": false,
5
5
  "main": "out/init.luau",
6
6
  "types": "out/index.d.ts",
7
+ "source": "src/index.ts",
7
8
  "files": [
9
+ "default.project.json",
8
10
  "out",
11
+ "src",
9
12
  "README.md"
10
13
  ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/astra-void/lattice-ui.git"
17
+ },
11
18
  "dependencies": {
12
- "@lattice-ui/core": "0.4.3",
13
- "@lattice-ui/layer": "0.4.3"
19
+ "@lattice-ui/core": "0.5.0-next.1",
20
+ "@lattice-ui/focus": "0.5.0-next.1",
21
+ "@lattice-ui/layer": "0.5.0-next.1",
22
+ "@lattice-ui/motion": "0.5.0-next.1"
14
23
  },
15
24
  "devDependencies": {
16
25
  "@rbxts/react": "17.3.7-ts.1",
@@ -22,6 +31,8 @@
22
31
  },
23
32
  "scripts": {
24
33
  "build": "rbxtsc -p tsconfig.json",
34
+ "lint": "eslint .",
35
+ "lint:fix": "eslint . --fix",
25
36
  "typecheck": "tsc -p tsconfig.typecheck.json",
26
37
  "watch": "rbxtsc -p tsconfig.json -w"
27
38
  }
@@ -0,0 +1,89 @@
1
+ import { React, Slot } from "@lattice-ui/core";
2
+ import { Presence } from "@lattice-ui/layer";
3
+ import { createSurfaceRevealRecipe, type PresenceMotionConfig, usePresenceMotion } from "@lattice-ui/motion";
4
+ import { useTabsContext } from "./context";
5
+ import { createTabsContentName } from "./internals/ids";
6
+ import type { TabsContentProps } from "./types";
7
+
8
+ function TabsContentImpl(props: {
9
+ motionPresent: boolean;
10
+ visible: boolean;
11
+ transition?: PresenceMotionConfig;
12
+ onExitComplete?: () => void;
13
+ value: string;
14
+ asChild?: boolean;
15
+ children?: React.ReactNode;
16
+ }) {
17
+ const contentName = createTabsContentName(props.value);
18
+ const defaultTransition = React.useMemo(() => createSurfaceRevealRecipe(), []);
19
+
20
+ const config = React.useMemo(() => {
21
+ if (!props.transition) return defaultTransition;
22
+ return props.transition;
23
+ }, [defaultTransition, props.transition]);
24
+
25
+ const motionRef = usePresenceMotion<Frame>(props.motionPresent, config, props.onExitComplete);
26
+
27
+ if (props.asChild) {
28
+ const child = props.children;
29
+ if (!React.isValidElement(child)) {
30
+ error("[TabsContent] `asChild` requires a child element.");
31
+ }
32
+
33
+ return (
34
+ <Slot Name={contentName} Visible={props.visible} ref={motionRef}>
35
+ {child}
36
+ </Slot>
37
+ );
38
+ }
39
+
40
+ return (
41
+ <frame
42
+ BackgroundTransparency={1}
43
+ BorderSizePixel={0}
44
+ Size={UDim2.fromOffset(0, 0)}
45
+ Visible={props.visible}
46
+ ref={motionRef}
47
+ >
48
+ {props.children}
49
+ </frame>
50
+ );
51
+ }
52
+
53
+ export function TabsContent(props: TabsContentProps) {
54
+ const tabsContext = useTabsContext();
55
+ const selected = tabsContext.value === props.value;
56
+ const forceMount = props.forceMount === true;
57
+
58
+ if (forceMount) {
59
+ return (
60
+ <TabsContentImpl
61
+ asChild={props.asChild}
62
+ motionPresent={selected}
63
+ transition={props.transition}
64
+ value={props.value}
65
+ visible={selected}
66
+ >
67
+ {props.children}
68
+ </TabsContentImpl>
69
+ );
70
+ }
71
+
72
+ return (
73
+ <Presence
74
+ present={selected}
75
+ render={(state) => (
76
+ <TabsContentImpl
77
+ asChild={props.asChild}
78
+ motionPresent={state.isPresent}
79
+ onExitComplete={state.onExitComplete}
80
+ transition={props.transition}
81
+ value={props.value}
82
+ visible={true}
83
+ >
84
+ {props.children}
85
+ </TabsContentImpl>
86
+ )}
87
+ />
88
+ );
89
+ }
@@ -0,0 +1,19 @@
1
+ import { React, Slot } from "@lattice-ui/core";
2
+ import type { TabsListProps } from "./types";
3
+
4
+ export function TabsList(props: TabsListProps) {
5
+ if (props.asChild) {
6
+ const child = props.children;
7
+ if (!React.isValidElement(child)) {
8
+ error("[TabsList] `asChild` requires a child element.");
9
+ }
10
+
11
+ return <Slot>{child}</Slot>;
12
+ }
13
+
14
+ return (
15
+ <frame BackgroundTransparency={1} BorderSizePixel={0} Size={UDim2.fromOffset(0, 0)}>
16
+ {props.children}
17
+ </frame>
18
+ );
19
+ }
@@ -0,0 +1,124 @@
1
+ import { React, useControllableState } from "@lattice-ui/core";
2
+ import {
3
+ findOrderedSelectionEntry,
4
+ focusOrderedSelectionEntry,
5
+ getOrderedSelectionEntries,
6
+ getRelativeOrderedSelectionEntry,
7
+ } from "@lattice-ui/focus";
8
+ import { TabsContextProvider } from "./context";
9
+ import type { TabsProps, TabsTriggerRegistration } from "./types";
10
+
11
+ function resolveNextValue(
12
+ currentValue: string | undefined,
13
+ orderedTriggers: Array<TabsTriggerRegistration>,
14
+ fallbackOrder: number | undefined,
15
+ ) {
16
+ const enabled = orderedTriggers.filter((trigger) => !trigger.getDisabled());
17
+ if (enabled.size() === 0) {
18
+ return undefined;
19
+ }
20
+
21
+ if (currentValue === undefined) {
22
+ return enabled[0]?.value;
23
+ }
24
+
25
+ const selectedEnabled = enabled.find((trigger) => trigger.value === currentValue);
26
+ if (selectedEnabled) {
27
+ return selectedEnabled.value;
28
+ }
29
+
30
+ const selected = orderedTriggers.find((trigger) => trigger.value === currentValue);
31
+ const anchorOrder = selected?.order ?? fallbackOrder;
32
+ if (anchorOrder !== undefined) {
33
+ const after = enabled.find((trigger) => trigger.order > anchorOrder);
34
+ if (after) {
35
+ return after.value;
36
+ }
37
+ }
38
+
39
+ return enabled[0]?.value;
40
+ }
41
+
42
+ export function TabsRoot(props: TabsProps) {
43
+ const orientation = props.orientation ?? "horizontal";
44
+ const [value, setValueState] = useControllableState<string | undefined>({
45
+ value: props.value,
46
+ defaultValue: props.defaultValue,
47
+ onChange: (nextValue) => {
48
+ if (nextValue !== undefined) {
49
+ props.onValueChange?.(nextValue);
50
+ }
51
+ },
52
+ });
53
+
54
+ const triggerRegistryRef = React.useRef<Array<TabsTriggerRegistration>>([]);
55
+ const lastSelectedOrderRef = React.useRef<number>();
56
+ const [registryRevision, setRegistryRevision] = React.useState(0);
57
+
58
+ const registerTrigger = React.useCallback((trigger: TabsTriggerRegistration) => {
59
+ triggerRegistryRef.current.push(trigger);
60
+ setRegistryRevision((revision) => revision + 1);
61
+
62
+ return () => {
63
+ const index = triggerRegistryRef.current.findIndex((entry) => entry.id === trigger.id);
64
+ if (index >= 0) {
65
+ triggerRegistryRef.current.remove(index);
66
+ setRegistryRevision((revision) => revision + 1);
67
+ }
68
+ };
69
+ }, []);
70
+
71
+ const setValue = React.useCallback(
72
+ (nextValue: string) => {
73
+ const orderedTriggers = getOrderedSelectionEntries(triggerRegistryRef.current);
74
+ const selected = orderedTriggers.find((trigger) => trigger.value === nextValue && !trigger.getDisabled());
75
+ if (selected) {
76
+ lastSelectedOrderRef.current = selected.order;
77
+ }
78
+
79
+ setValueState(nextValue);
80
+ },
81
+ [setValueState],
82
+ );
83
+
84
+ const moveSelection = React.useCallback(
85
+ (fromValue: string, direction: -1 | 1) => {
86
+ const currentTrigger =
87
+ findOrderedSelectionEntry(triggerRegistryRef.current, (trigger) => trigger.value === fromValue) ?? undefined;
88
+ const nextTrigger = getRelativeOrderedSelectionEntry(triggerRegistryRef.current, currentTrigger?.id, direction);
89
+ if (!nextTrigger) {
90
+ return;
91
+ }
92
+
93
+ focusOrderedSelectionEntry(nextTrigger);
94
+ setValue(nextTrigger.value);
95
+ },
96
+ [setValue],
97
+ );
98
+
99
+ React.useEffect(() => {
100
+ const orderedTriggers = getOrderedSelectionEntries(triggerRegistryRef.current);
101
+ const selected = orderedTriggers.find((trigger) => trigger.value === value && !trigger.getDisabled());
102
+ if (selected) {
103
+ lastSelectedOrderRef.current = selected.order;
104
+ }
105
+
106
+ const nextValue = resolveNextValue(value, orderedTriggers, lastSelectedOrderRef.current);
107
+ if (nextValue !== value) {
108
+ setValueState(nextValue);
109
+ }
110
+ }, [registryRevision, setValueState, value]);
111
+
112
+ const contextValue = React.useMemo(
113
+ () => ({
114
+ value,
115
+ orientation,
116
+ setValue,
117
+ registerTrigger,
118
+ moveSelection,
119
+ }),
120
+ [moveSelection, orientation, registerTrigger, setValue, value],
121
+ );
122
+
123
+ return <TabsContextProvider value={contextValue}>{props.children}</TabsContextProvider>;
124
+ }
@@ -0,0 +1,171 @@
1
+ import { React, Slot } from "@lattice-ui/core";
2
+ import { useFocusNode } from "@lattice-ui/focus";
3
+ import { createSelectionResponseRecipe, useResponseMotion } from "@lattice-ui/motion";
4
+ import { useTabsContext } from "./context";
5
+ import { createTabsTriggerName } from "./internals/ids";
6
+ import type { TabsTriggerProps } from "./types";
7
+
8
+ let nextTriggerId = 0;
9
+ let nextTriggerOrder = 0;
10
+
11
+ function toGuiObject(instance: Instance | undefined) {
12
+ if (!instance || !instance.IsA("GuiObject")) {
13
+ return undefined;
14
+ }
15
+
16
+ return instance;
17
+ }
18
+
19
+ export function TabsTrigger(props: TabsTriggerProps) {
20
+ const tabsContext = useTabsContext();
21
+ const triggerRef = React.useRef<GuiObject>();
22
+ const selected = tabsContext.value === props.value;
23
+ const disabled = props.disabled === true;
24
+ const disabledRef = React.useRef(disabled);
25
+
26
+ React.useEffect(() => {
27
+ disabledRef.current = disabled;
28
+ }, [disabled]);
29
+
30
+ const triggerIdRef = React.useRef(0);
31
+ if (triggerIdRef.current === 0) {
32
+ nextTriggerId += 1;
33
+ triggerIdRef.current = nextTriggerId;
34
+ }
35
+
36
+ const triggerOrderRef = React.useRef(0);
37
+ if (triggerOrderRef.current === 0) {
38
+ nextTriggerOrder += 1;
39
+ triggerOrderRef.current = nextTriggerOrder;
40
+ }
41
+
42
+ React.useEffect(() => {
43
+ return tabsContext.registerTrigger({
44
+ id: triggerIdRef.current,
45
+ value: props.value,
46
+ ref: triggerRef,
47
+ order: triggerOrderRef.current,
48
+ getDisabled: () => disabledRef.current,
49
+ });
50
+ }, [props.value, tabsContext]);
51
+
52
+ useFocusNode({
53
+ ref: triggerRef,
54
+ getDisabled: () => disabledRef.current,
55
+ });
56
+
57
+ const motionRef = useResponseMotion<GuiObject>(
58
+ selected,
59
+ {
60
+ active: { BackgroundColor3: Color3.fromRGB(86, 137, 245), TextColor3: Color3.fromRGB(235, 240, 248) },
61
+ inactive: { BackgroundColor3: Color3.fromRGB(47, 53, 68), TextColor3: Color3.fromRGB(136, 144, 159) },
62
+ },
63
+ createSelectionResponseRecipe(),
64
+ );
65
+ const setTriggerRef = React.useCallback(
66
+ (instance: Instance | undefined) => {
67
+ const nextTrigger = toGuiObject(instance);
68
+ triggerRef.current = nextTrigger;
69
+ motionRef.current = nextTrigger;
70
+ },
71
+ [motionRef],
72
+ );
73
+
74
+ const handleActivated = React.useCallback(() => {
75
+ if (disabled) {
76
+ return;
77
+ }
78
+
79
+ tabsContext.setValue(props.value);
80
+ }, [disabled, props.value, tabsContext]);
81
+
82
+ const handleSelectionGained = React.useCallback(() => {
83
+ if (disabled) {
84
+ return;
85
+ }
86
+
87
+ tabsContext.setValue(props.value);
88
+ }, [disabled, props.value, tabsContext]);
89
+
90
+ const handleInputBegan = React.useCallback(
91
+ (_rbx: TextButton, inputObject: InputObject) => {
92
+ if (disabled) {
93
+ return;
94
+ }
95
+
96
+ const keyCode = inputObject.KeyCode;
97
+ const direction =
98
+ tabsContext.orientation === "horizontal"
99
+ ? keyCode === Enum.KeyCode.Left
100
+ ? -1
101
+ : keyCode === Enum.KeyCode.Right
102
+ ? 1
103
+ : undefined
104
+ : keyCode === Enum.KeyCode.Up
105
+ ? -1
106
+ : keyCode === Enum.KeyCode.Down
107
+ ? 1
108
+ : undefined;
109
+
110
+ if (direction !== undefined) {
111
+ tabsContext.moveSelection(props.value, direction);
112
+ return;
113
+ }
114
+
115
+ if (keyCode !== Enum.KeyCode.Return && keyCode !== Enum.KeyCode.Space) {
116
+ return;
117
+ }
118
+
119
+ tabsContext.setValue(props.value);
120
+ },
121
+ [disabled, props.value, tabsContext],
122
+ );
123
+
124
+ const eventHandlers = React.useMemo(
125
+ () => ({
126
+ Activated: handleActivated,
127
+ InputBegan: handleInputBegan,
128
+ SelectionGained: handleSelectionGained,
129
+ }),
130
+ [handleActivated, handleInputBegan, handleSelectionGained],
131
+ );
132
+
133
+ const triggerName = React.useMemo(() => createTabsTriggerName(props.value), [props.value]);
134
+
135
+ if (props.asChild) {
136
+ const child = props.children;
137
+ if (!child) {
138
+ error("[TabsTrigger] `asChild` requires a child element.");
139
+ }
140
+
141
+ return (
142
+ <Slot Active={!disabled} Event={eventHandlers} Name={triggerName} Selectable={!disabled} ref={setTriggerRef}>
143
+ {child}
144
+ </Slot>
145
+ );
146
+ }
147
+
148
+ return (
149
+ <textbutton
150
+ Active={!disabled}
151
+ AutoButtonColor={false}
152
+ BackgroundColor3={selected ? Color3.fromRGB(86, 137, 245) : Color3.fromRGB(47, 53, 68)}
153
+ BorderSizePixel={0}
154
+ Event={eventHandlers}
155
+ Selectable={!disabled}
156
+ Size={UDim2.fromOffset(132, 34)}
157
+ Text={props.value}
158
+ TextColor3={
159
+ selected
160
+ ? Color3.fromRGB(235, 240, 248)
161
+ : disabled
162
+ ? Color3.fromRGB(136, 144, 159)
163
+ : Color3.fromRGB(235, 240, 248)
164
+ }
165
+ TextSize={15}
166
+ ref={setTriggerRef}
167
+ >
168
+ {props.children}
169
+ </textbutton>
170
+ );
171
+ }
@@ -0,0 +1,6 @@
1
+ import { createStrictContext } from "@lattice-ui/core";
2
+ import type { TabsContextValue } from "./types";
3
+
4
+ const [TabsContextProvider, useTabsContext] = createStrictContext<TabsContextValue>("Tabs");
5
+
6
+ export { TabsContextProvider, useTabsContext };
@@ -0,0 +1,13 @@
1
+ function sanitizeValue(value: string) {
2
+ const lowered = string.lower(value);
3
+ const sanitized = string.gsub(lowered, "[^%w_%-]", "-")[0];
4
+ return sanitized.size() > 0 ? sanitized : "tab";
5
+ }
6
+
7
+ export function createTabsTriggerName(value: string) {
8
+ return `TabsTrigger:${sanitizeValue(value)}`;
9
+ }
10
+
11
+ export function createTabsContentName(value: string) {
12
+ return `TabsContent:${sanitizeValue(value)}`;
13
+ }
@@ -0,0 +1,49 @@
1
+ import type { PresenceMotionConfig } from "@lattice-ui/motion";
2
+ import type React from "@rbxts/react";
3
+
4
+ export type TabsSetValue = (value: string) => void;
5
+ export type TabsOrientation = "horizontal" | "vertical";
6
+
7
+ export type TabsTriggerRegistration = {
8
+ id: number;
9
+ value: string;
10
+ ref: React.MutableRefObject<GuiObject | undefined>;
11
+ order: number;
12
+ getDisabled: () => boolean;
13
+ };
14
+
15
+ export type TabsContextValue = {
16
+ value?: string;
17
+ orientation: TabsOrientation;
18
+ setValue: TabsSetValue;
19
+ registerTrigger: (trigger: TabsTriggerRegistration) => () => void;
20
+ moveSelection: (fromValue: string, direction: -1 | 1) => void;
21
+ };
22
+
23
+ export type TabsProps = {
24
+ value?: string;
25
+ defaultValue?: string;
26
+ onValueChange?: (value: string) => void;
27
+ orientation?: TabsOrientation;
28
+ children?: React.ReactNode;
29
+ };
30
+
31
+ export type TabsListProps = {
32
+ asChild?: boolean;
33
+ children?: React.ReactNode;
34
+ };
35
+
36
+ export type TabsTriggerProps = {
37
+ value: string;
38
+ asChild?: boolean;
39
+ disabled?: boolean;
40
+ children?: React.ReactElement;
41
+ };
42
+
43
+ export type TabsContentProps = {
44
+ transition?: PresenceMotionConfig;
45
+ value: string;
46
+ asChild?: boolean;
47
+ forceMount?: boolean;
48
+ children?: React.ReactNode;
49
+ };
package/src/index.ts ADDED
@@ -0,0 +1,20 @@
1
+ import { TabsContent } from "./Tabs/TabsContent";
2
+ import { TabsList } from "./Tabs/TabsList";
3
+ import { TabsRoot } from "./Tabs/TabsRoot";
4
+ import { TabsTrigger } from "./Tabs/TabsTrigger";
5
+
6
+ export const Tabs = {
7
+ Root: TabsRoot,
8
+ List: TabsList,
9
+ Trigger: TabsTrigger,
10
+ Content: TabsContent,
11
+ } as const;
12
+
13
+ export type {
14
+ TabsContentProps,
15
+ TabsContextValue,
16
+ TabsListProps,
17
+ TabsOrientation,
18
+ TabsProps,
19
+ TabsTriggerProps,
20
+ } from "./Tabs/types";