@lattice-ui/tabs 0.4.4 → 0.5.0-next.2

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,33 @@ 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 usePresenceMotionController = _motion.usePresenceMotionController
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 motion = usePresenceMotionController({
24
+ present = props.present,
25
+ forceMount = props.forceMount,
26
+ config = config,
27
+ onExitComplete = props.onExitComplete,
28
+ })
29
+ local mounted = motion.mounted
30
+ local visible = mounted and (motion.present or motion.phase ~= "exited")
31
+ if not mounted then
32
+ return nil
33
+ end
11
34
  if props.asChild then
12
35
  local child = props.children
13
36
  if not React.isValidElement(child) then
@@ -15,38 +38,40 @@ local function TabsContentImpl(props)
15
38
  end
16
39
  return React.createElement(Slot, {
17
40
  Name = contentName,
18
- Visible = props.visible,
41
+ Visible = visible,
42
+ ref = motion.ref,
19
43
  }, child)
20
44
  end
21
45
  return React.createElement("frame", {
22
46
  BackgroundTransparency = 1,
23
47
  BorderSizePixel = 0,
24
48
  Size = UDim2.fromOffset(0, 0),
25
- Visible = props.visible,
49
+ Visible = visible,
50
+ ref = motion.ref,
26
51
  }, props.children)
27
52
  end
28
53
  local function TabsContent(props)
29
54
  local tabsContext = useTabsContext()
30
55
  local selected = tabsContext.value == props.value
31
56
  local forceMount = props.forceMount == true
32
- if not selected and not forceMount then
33
- return nil
34
- end
35
57
  if forceMount then
36
58
  return React.createElement(TabsContentImpl, {
37
59
  asChild = props.asChild,
60
+ forceMount = true,
61
+ present = selected,
62
+ transition = props.transition,
38
63
  value = props.value,
39
- visible = selected,
40
64
  }, props.children)
41
65
  end
42
66
  return React.createElement(Presence, {
43
- exitFallbackMs = 0,
44
67
  present = selected,
45
68
  render = function(state)
46
69
  return React.createElement(TabsContentImpl, {
47
70
  asChild = props.asChild,
71
+ onExitComplete = state.onExitComplete,
72
+ present = state.isPresent,
73
+ transition = props.transition,
48
74
  value = props.value,
49
- visible = state.isPresent,
50
75
  }, props.children)
51
76
  end,
52
77
  })
@@ -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
@@ -20,9 +23,7 @@ local function TabsTrigger(props)
20
23
  local selected = tabsContext.value == props.value
21
24
  local disabled = props.disabled == true
22
25
  local disabledRef = React.useRef(disabled)
23
- React.useEffect(function()
24
- disabledRef.current = disabled
25
- end, { disabled })
26
+ disabledRef.current = disabled
26
27
  local triggerIdRef = React.useRef(0)
27
28
  if triggerIdRef.current == 0 then
28
29
  nextTriggerId += 1
@@ -43,16 +44,28 @@ local function TabsTrigger(props)
43
44
  return disabledRef.current
44
45
  end,
45
46
  })
46
- end, { props.value, tabsContext })
47
+ end, { disabled, props.value, tabsContext })
47
48
  useFocusNode({
48
49
  ref = triggerRef,
49
50
  getDisabled = function()
50
51
  return disabledRef.current
51
52
  end,
52
53
  })
54
+ local motionRef = useResponseMotion(selected, {
55
+ active = {
56
+ BackgroundColor3 = Color3.fromRGB(86, 137, 245),
57
+ TextColor3 = Color3.fromRGB(235, 240, 248),
58
+ },
59
+ inactive = {
60
+ BackgroundColor3 = Color3.fromRGB(47, 53, 68),
61
+ TextColor3 = Color3.fromRGB(136, 144, 159),
62
+ },
63
+ }, createSelectionResponseRecipe())
53
64
  local setTriggerRef = React.useCallback(function(instance)
54
- triggerRef.current = toGuiObject(instance)
55
- end, {})
65
+ local nextTrigger = toGuiObject(instance)
66
+ triggerRef.current = nextTrigger
67
+ motionRef.current = nextTrigger
68
+ end, { motionRef })
56
69
  local handleActivated = React.useCallback(function()
57
70
  if disabled then
58
71
  return nil
@@ -112,7 +125,7 @@ local function TabsTrigger(props)
112
125
  Selectable = not disabled,
113
126
  Size = UDim2.fromOffset(132, 34),
114
127
  Text = props.value,
115
- TextColor3 = if disabled then Color3.fromRGB(136, 144, 159) else Color3.fromRGB(235, 240, 248),
128
+ TextColor3 = if selected then Color3.fromRGB(235, 240, 248) elseif disabled then Color3.fromRGB(136, 144, 159) else Color3.fromRGB(235, 240, 248),
116
129
  TextSize = 15,
117
130
  ref = setTriggerRef,
118
131
  }, 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.4",
3
+ "version": "0.5.0-next.2",
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/layer": "0.4.4",
13
- "@lattice-ui/core": "0.4.4"
19
+ "@lattice-ui/core": "0.5.0-next.2",
20
+ "@lattice-ui/layer": "0.5.0-next.2",
21
+ "@lattice-ui/motion": "0.5.0-next.2",
22
+ "@lattice-ui/focus": "0.5.0-next.2"
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,100 @@
1
+ import { React, Slot } from "@lattice-ui/core";
2
+ import { Presence } from "@lattice-ui/layer";
3
+ import { createSurfaceRevealRecipe, type PresenceMotionConfig, usePresenceMotionController } 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
+ present: boolean;
10
+ forceMount?: 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 motion = usePresenceMotionController<Frame>({
26
+ present: props.present,
27
+ forceMount: props.forceMount,
28
+ config,
29
+ onExitComplete: props.onExitComplete,
30
+ });
31
+
32
+ const mounted = motion.mounted;
33
+ const visible = mounted && (motion.present || motion.phase !== "exited");
34
+
35
+ if (!mounted) {
36
+ return undefined;
37
+ }
38
+
39
+ if (props.asChild) {
40
+ const child = props.children;
41
+ if (!React.isValidElement(child)) {
42
+ error("[TabsContent] `asChild` requires a child element.");
43
+ }
44
+
45
+ return (
46
+ <Slot Name={contentName} Visible={visible} ref={motion.ref}>
47
+ {child}
48
+ </Slot>
49
+ );
50
+ }
51
+
52
+ return (
53
+ <frame
54
+ BackgroundTransparency={1}
55
+ BorderSizePixel={0}
56
+ Size={UDim2.fromOffset(0, 0)}
57
+ Visible={visible}
58
+ ref={motion.ref}
59
+ >
60
+ {props.children}
61
+ </frame>
62
+ );
63
+ }
64
+
65
+ export function TabsContent(props: TabsContentProps) {
66
+ const tabsContext = useTabsContext();
67
+ const selected = tabsContext.value === props.value;
68
+ const forceMount = props.forceMount === true;
69
+
70
+ if (forceMount) {
71
+ return (
72
+ <TabsContentImpl
73
+ asChild={props.asChild}
74
+ forceMount={true}
75
+ present={selected}
76
+ transition={props.transition}
77
+ value={props.value}
78
+ >
79
+ {props.children}
80
+ </TabsContentImpl>
81
+ );
82
+ }
83
+
84
+ return (
85
+ <Presence
86
+ present={selected}
87
+ render={(state) => (
88
+ <TabsContentImpl
89
+ asChild={props.asChild}
90
+ onExitComplete={state.onExitComplete}
91
+ present={state.isPresent}
92
+ transition={props.transition}
93
+ value={props.value}
94
+ >
95
+ {props.children}
96
+ </TabsContentImpl>
97
+ )}
98
+ />
99
+ );
100
+ }
@@ -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,168 @@
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
+ disabledRef.current = disabled;
26
+
27
+ const triggerIdRef = React.useRef(0);
28
+ if (triggerIdRef.current === 0) {
29
+ nextTriggerId += 1;
30
+ triggerIdRef.current = nextTriggerId;
31
+ }
32
+
33
+ const triggerOrderRef = React.useRef(0);
34
+ if (triggerOrderRef.current === 0) {
35
+ nextTriggerOrder += 1;
36
+ triggerOrderRef.current = nextTriggerOrder;
37
+ }
38
+
39
+ React.useEffect(() => {
40
+ return tabsContext.registerTrigger({
41
+ id: triggerIdRef.current,
42
+ value: props.value,
43
+ ref: triggerRef,
44
+ order: triggerOrderRef.current,
45
+ getDisabled: () => disabledRef.current,
46
+ });
47
+ }, [disabled, props.value, tabsContext]);
48
+
49
+ useFocusNode({
50
+ ref: triggerRef,
51
+ getDisabled: () => disabledRef.current,
52
+ });
53
+
54
+ const motionRef = useResponseMotion<GuiObject>(
55
+ selected,
56
+ {
57
+ active: { BackgroundColor3: Color3.fromRGB(86, 137, 245), TextColor3: Color3.fromRGB(235, 240, 248) },
58
+ inactive: { BackgroundColor3: Color3.fromRGB(47, 53, 68), TextColor3: Color3.fromRGB(136, 144, 159) },
59
+ },
60
+ createSelectionResponseRecipe(),
61
+ );
62
+ const setTriggerRef = React.useCallback(
63
+ (instance: Instance | undefined) => {
64
+ const nextTrigger = toGuiObject(instance);
65
+ triggerRef.current = nextTrigger;
66
+ motionRef.current = nextTrigger;
67
+ },
68
+ [motionRef],
69
+ );
70
+
71
+ const handleActivated = React.useCallback(() => {
72
+ if (disabled) {
73
+ return;
74
+ }
75
+
76
+ tabsContext.setValue(props.value);
77
+ }, [disabled, props.value, tabsContext]);
78
+
79
+ const handleSelectionGained = React.useCallback(() => {
80
+ if (disabled) {
81
+ return;
82
+ }
83
+
84
+ tabsContext.setValue(props.value);
85
+ }, [disabled, props.value, tabsContext]);
86
+
87
+ const handleInputBegan = React.useCallback(
88
+ (_rbx: TextButton, inputObject: InputObject) => {
89
+ if (disabled) {
90
+ return;
91
+ }
92
+
93
+ const keyCode = inputObject.KeyCode;
94
+ const direction =
95
+ tabsContext.orientation === "horizontal"
96
+ ? keyCode === Enum.KeyCode.Left
97
+ ? -1
98
+ : keyCode === Enum.KeyCode.Right
99
+ ? 1
100
+ : undefined
101
+ : keyCode === Enum.KeyCode.Up
102
+ ? -1
103
+ : keyCode === Enum.KeyCode.Down
104
+ ? 1
105
+ : undefined;
106
+
107
+ if (direction !== undefined) {
108
+ tabsContext.moveSelection(props.value, direction);
109
+ return;
110
+ }
111
+
112
+ if (keyCode !== Enum.KeyCode.Return && keyCode !== Enum.KeyCode.Space) {
113
+ return;
114
+ }
115
+
116
+ tabsContext.setValue(props.value);
117
+ },
118
+ [disabled, props.value, tabsContext],
119
+ );
120
+
121
+ const eventHandlers = React.useMemo(
122
+ () => ({
123
+ Activated: handleActivated,
124
+ InputBegan: handleInputBegan,
125
+ SelectionGained: handleSelectionGained,
126
+ }),
127
+ [handleActivated, handleInputBegan, handleSelectionGained],
128
+ );
129
+
130
+ const triggerName = React.useMemo(() => createTabsTriggerName(props.value), [props.value]);
131
+
132
+ if (props.asChild) {
133
+ const child = props.children;
134
+ if (!child) {
135
+ error("[TabsTrigger] `asChild` requires a child element.");
136
+ }
137
+
138
+ return (
139
+ <Slot Active={!disabled} Event={eventHandlers} Name={triggerName} Selectable={!disabled} ref={setTriggerRef}>
140
+ {child}
141
+ </Slot>
142
+ );
143
+ }
144
+
145
+ return (
146
+ <textbutton
147
+ Active={!disabled}
148
+ AutoButtonColor={false}
149
+ BackgroundColor3={selected ? Color3.fromRGB(86, 137, 245) : Color3.fromRGB(47, 53, 68)}
150
+ BorderSizePixel={0}
151
+ Event={eventHandlers}
152
+ Selectable={!disabled}
153
+ Size={UDim2.fromOffset(132, 34)}
154
+ Text={props.value}
155
+ TextColor3={
156
+ selected
157
+ ? Color3.fromRGB(235, 240, 248)
158
+ : disabled
159
+ ? Color3.fromRGB(136, 144, 159)
160
+ : Color3.fromRGB(235, 240, 248)
161
+ }
162
+ TextSize={15}
163
+ ref={setTriggerRef}
164
+ >
165
+ {props.children}
166
+ </textbutton>
167
+ );
168
+ }
@@ -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";