Skip to content

Commit a4666d0

Browse files
feat(ui): add JContainer component with max-width constraints (#613)
Add responsive container component following Tailwind/Bootstrap patterns: - JContainer with ContainerSize enum (Xs/Sm/Md/Lg/Xl/Full) - Fluent API: WithSize(), WithHorizontalPadding(), WithResponsivePadding(), Fluid() - Container tokens in Tokens.cs (480/640/768/1024/1280px) - CSS classes for styling: j-container--xs/sm/md/lg/xl/full - Wrap PanelUI and BootstrapEditorUI content in JContainer(Xs) - Rename .j-container to .j-panel-base in base.uss to avoid conflicts Also adds unit testing requirements to coding guidelines and comprehensive tests for JContainer (45+ test cases covering all functionality). Signed-off-by: JasonXuDeveloper - 傑 <jason@xgamedev.net> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 00ba108 commit a4666d0

File tree

15 files changed

+870
-37
lines changed

15 files changed

+870
-37
lines changed

.claude/rules/coding-patterns.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,51 @@ public abstract class ConfigBase<T> : ScriptableObject where T : ConfigBase<T>
4848
- Handle Unity domain reloads properly (state resets on recompile)
4949
- Use `SessionState` or `EditorPrefs` for persistent editor state
5050
- Clean up resources in `EditorApplication.quitting`
51+
52+
## Unit Testing
53+
54+
**IMPORTANT**: When adding or modifying features in non-core packages (JEngine.UI, JEngine.Util), you MUST add unit tests.
55+
56+
### Test Location
57+
58+
Tests go in `Packages/com.jasonxudeveloper.jengine.<package>/Tests/Editor/`:
59+
- Components: `Tests/Editor/Components/<Category>/<ComponentName>Tests.cs`
60+
- Theming: `Tests/Editor/Theming/<ClassName>Tests.cs`
61+
- Utilities: `Tests/Editor/Utilities/<ClassName>Tests.cs`
62+
63+
### Test Pattern
64+
65+
Follow the existing test structure:
66+
```csharp
67+
using NUnit.Framework;
68+
using JEngine.UI.Editor.Components.Layout;
69+
70+
namespace JEngine.UI.Tests.Editor.Components.Layout
71+
{
72+
[TestFixture]
73+
public class JContainerTests
74+
{
75+
private JContainer _container;
76+
77+
[SetUp]
78+
public void SetUp()
79+
{
80+
_container = new JContainer();
81+
}
82+
83+
[Test]
84+
public void Constructor_Default_AddsBaseClass()
85+
{
86+
Assert.IsTrue(_container.ClassListContains("j-container"));
87+
}
88+
}
89+
}
90+
```
91+
92+
### Test Coverage Requirements
93+
94+
- Constructor behavior (default and parameterized)
95+
- All public methods and properties
96+
- Fluent API chaining
97+
- Edge cases and error conditions
98+
- Inherited behavior from base classes

CLAUDE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ Document all public APIs with XML comments.
8181
- Run tests via Unity Test Runner
8282
- Use `[UnityTest]` with `UniTask.ToCoroutine()` for async tests
8383
- Editor code should check `TestRunnerCallbacks.IsRunningTests`
84+
- **IMPORTANT**: Non-core packages (JEngine.UI, JEngine.Util) require unit tests for new/modified features
8485

8586
## Code Review Checklist
8687

@@ -89,3 +90,4 @@ Document all public APIs with XML comments.
8990
- [ ] Uses UniTask for async (not Task)
9091
- [ ] Thread-safe where needed
9192
- [ ] Proper resource cleanup
93+
- [ ] Unit tests added for non-core package changes
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
// JContainer.cs
2+
//
3+
// Author:
4+
// JasonXuDeveloper <jason@xgamedev.net>
5+
//
6+
// Copyright (c) 2025 JEngine
7+
//
8+
// Permission is hereby granted, free of charge, to any person obtaining a copy
9+
// of this software and associated documentation files (the "Software"), to deal
10+
// in the Software without restriction, including without limitation the rights
11+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12+
// copies of the Software, and to permit persons to whom the Software is
13+
// furnished to do so, subject to the following conditions:
14+
//
15+
// The above copyright notice and this permission notice shall be included in
16+
// all copies or substantial portions of the Software.
17+
//
18+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24+
// THE SOFTWARE.
25+
26+
using JEngine.UI.Editor.Theming;
27+
using UnityEngine.UIElements;
28+
29+
namespace JEngine.UI.Editor.Components.Layout
30+
{
31+
/// <summary>
32+
/// Responsive container component with max-width constraints.
33+
/// Centers content horizontally with configurable maximum width.
34+
/// Based on Tailwind CSS container patterns.
35+
/// </summary>
36+
public class JContainer : JComponent
37+
{
38+
private ContainerSize _size;
39+
40+
/// <summary>
41+
/// Creates a new container with the default large (1024px) size.
42+
/// </summary>
43+
public JContainer() : this(ContainerSize.Lg)
44+
{
45+
}
46+
47+
/// <summary>
48+
/// Creates a new container with the specified size.
49+
/// </summary>
50+
/// <param name="size">The container size (max-width constraint).</param>
51+
public JContainer(ContainerSize size) : base("j-container")
52+
{
53+
// Set width to 100% and center with auto margins
54+
style.width = Length.Percent(100);
55+
style.marginLeft = StyleKeyword.Auto;
56+
style.marginRight = StyleKeyword.Auto;
57+
58+
SetSize(size);
59+
}
60+
61+
/// <summary>
62+
/// Gets or sets the container size.
63+
/// </summary>
64+
public ContainerSize Size
65+
{
66+
get => _size;
67+
set => SetSize(value);
68+
}
69+
70+
/// <summary>
71+
/// Sets the container size.
72+
/// </summary>
73+
/// <param name="size">The container size.</param>
74+
/// <returns>This container for chaining.</returns>
75+
public JContainer WithSize(ContainerSize size)
76+
{
77+
SetSize(size);
78+
return this;
79+
}
80+
81+
/// <summary>
82+
/// Sets horizontal padding on the container.
83+
/// </summary>
84+
/// <param name="padding">The padding value in pixels.</param>
85+
/// <returns>This container for chaining.</returns>
86+
public JContainer WithHorizontalPadding(float padding)
87+
{
88+
style.paddingLeft = padding;
89+
style.paddingRight = padding;
90+
return this;
91+
}
92+
93+
/// <summary>
94+
/// Applies responsive horizontal padding based on container size.
95+
/// Larger containers get more padding.
96+
/// </summary>
97+
/// <returns>This container for chaining.</returns>
98+
public JContainer WithResponsivePadding()
99+
{
100+
var padding = _size switch
101+
{
102+
ContainerSize.Xs => Tokens.Spacing.MD,
103+
ContainerSize.Sm => Tokens.Spacing.Lg,
104+
ContainerSize.Md => Tokens.Spacing.Xl,
105+
ContainerSize.Lg => Tokens.Spacing.Xxl,
106+
ContainerSize.Xl => Tokens.Spacing.Xxl,
107+
ContainerSize.Full => Tokens.Spacing.Lg,
108+
_ => Tokens.Spacing.Lg
109+
};
110+
111+
style.paddingLeft = padding;
112+
style.paddingRight = padding;
113+
return this;
114+
}
115+
116+
/// <summary>
117+
/// Sets the container to fluid mode (no max-width constraint).
118+
/// </summary>
119+
/// <returns>This container for chaining.</returns>
120+
public JContainer Fluid()
121+
{
122+
SetSize(ContainerSize.Full);
123+
return this;
124+
}
125+
126+
/// <summary>
127+
/// Adds child elements to this container.
128+
/// </summary>
129+
/// <param name="children">The elements to add.</param>
130+
/// <returns>This container for chaining.</returns>
131+
public new JContainer Add(params VisualElement[] children)
132+
{
133+
base.Add(children);
134+
return this;
135+
}
136+
137+
private void SetSize(ContainerSize size)
138+
{
139+
_size = size;
140+
141+
// Remove existing size classes
142+
RemoveFromClassList("j-container--xs");
143+
RemoveFromClassList("j-container--sm");
144+
RemoveFromClassList("j-container--md");
145+
RemoveFromClassList("j-container--lg");
146+
RemoveFromClassList("j-container--xl");
147+
RemoveFromClassList("j-container--full");
148+
149+
// Add new size class
150+
var sizeClass = size switch
151+
{
152+
ContainerSize.Xs => "j-container--xs",
153+
ContainerSize.Sm => "j-container--sm",
154+
ContainerSize.Md => "j-container--md",
155+
ContainerSize.Lg => "j-container--lg",
156+
ContainerSize.Xl => "j-container--xl",
157+
ContainerSize.Full => "j-container--full",
158+
_ => "j-container--lg"
159+
};
160+
AddToClassList(sizeClass);
161+
162+
// Also set max-width style directly for immediate effect
163+
if (size == ContainerSize.Full)
164+
{
165+
style.maxWidth = StyleKeyword.None;
166+
}
167+
else
168+
{
169+
var maxWidth = size switch
170+
{
171+
ContainerSize.Xs => Tokens.Container.Xs,
172+
ContainerSize.Sm => Tokens.Container.Sm,
173+
ContainerSize.Md => Tokens.Container.Md,
174+
ContainerSize.Lg => Tokens.Container.Lg,
175+
ContainerSize.Xl => Tokens.Container.Xl,
176+
_ => Tokens.Container.Lg
177+
};
178+
style.maxWidth = maxWidth;
179+
}
180+
}
181+
}
182+
}

UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Layout/JContainer.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Internal/BootstrapEditorUI.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ public static VisualElement CreateInspector(SerializedObject serializedObject, B
7676
root.style.paddingRight = Tokens.Spacing.MD;
7777
root.style.paddingBottom = Tokens.Spacing.MD;
7878

79+
// Centered container for compact inspector layout
80+
var container = new JContainer(ContainerSize.Xs);
81+
7982
var content = new JStack(GapSize.Sm);
8083

8184
// Header
@@ -98,7 +101,8 @@ public static VisualElement CreateInspector(SerializedObject serializedObject, B
98101
// UI Settings
99102
content.Add(CreateUISettingsSection());
100103

101-
root.Add(content);
104+
container.Add(content);
105+
root.Add(container);
102106

103107
// Register undo/redo callback
104108
Undo.undoRedoPerformed += OnUndoRedo;
@@ -131,6 +135,9 @@ private static void OnUndoRedo()
131135
// Rebuild the entire UI
132136
_currentRoot.Clear();
133137

138+
// Centered container for compact inspector layout
139+
var container = new JContainer(ContainerSize.Xs);
140+
134141
// Recreate content
135142
var content = new JStack(GapSize.Sm);
136143

@@ -144,7 +151,8 @@ private static void OnUndoRedo()
144151
content.Add(CreateSecuritySettingsSection());
145152
content.Add(CreateUISettingsSection());
146153

147-
_currentRoot.Add(content);
154+
container.Add(content);
155+
_currentRoot.Add(container);
148156
}
149157

150158
private static VisualElement CreateHeader()

UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Internal/PanelUI.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ public static VisualElement CreateContent(Panel panel, BuildManager buildManager
8080
var scrollView = new ScrollView(ScrollViewMode.Vertical);
8181
scrollView.style.flexGrow = 1;
8282

83+
// Centered container for compact panel layout
84+
var container = new JContainer(ContainerSize.Xs);
85+
8386
var content = new JStack(GapSize.Sm);
8487

8588
// Header
@@ -103,7 +106,8 @@ public static VisualElement CreateContent(Panel panel, BuildManager buildManager
103106
// Status Section
104107
content.Add(CreateStatusSection());
105108

106-
scrollView.Add(content);
109+
container.Add(content);
110+
scrollView.Add(container);
107111
root.Add(scrollView);
108112

109113
return root;

UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Styles/base.uss

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
* Base reset and common styles for JEngine Editor UI
33
*/
44

5-
/* Base container */
6-
.j-container {
5+
/* Base panel styling */
6+
.j-panel-base {
77
background-color: var(--j-bg-base);
88
color: var(--j-text-primary);
99
padding: var(--j-spacing-lg);

UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Styles/components.uss

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,41 @@
22
* Component-specific styles for JEngine Editor UI
33
*/
44

5+
/* ===========================
6+
CONTAINER COMPONENT
7+
=========================== */
8+
9+
/* JContainer - responsive max-width container with horizontal centering */
10+
.j-container {
11+
width: 100%;
12+
margin-left: auto;
13+
margin-right: auto;
14+
}
15+
16+
.j-container--xs {
17+
max-width: 480px;
18+
}
19+
20+
.j-container--sm {
21+
max-width: 640px;
22+
}
23+
24+
.j-container--md {
25+
max-width: 768px;
26+
}
27+
28+
.j-container--lg {
29+
max-width: 1024px;
30+
}
31+
32+
.j-container--xl {
33+
max-width: 1280px;
34+
}
35+
36+
.j-container--full {
37+
max-width: none;
38+
}
39+
540
/* ===========================
641
LAYOUT COMPONENTS
742
=========================== */

UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Theming/JTheme.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,4 +411,24 @@ public enum StatusType
411411
Warning,
412412
Error
413413
}
414+
415+
/// <summary>
416+
/// Container size options for max-width constraints.
417+
/// Based on Tailwind CSS breakpoints.
418+
/// </summary>
419+
public enum ContainerSize
420+
{
421+
/// <summary>480px - Mobile-like, narrow inspectors.</summary>
422+
Xs,
423+
/// <summary>640px - Compact panels, side docks.</summary>
424+
Sm,
425+
/// <summary>768px - Standard inspector width.</summary>
426+
Md,
427+
/// <summary>1024px - Wide panels (default).</summary>
428+
Lg,
429+
/// <summary>1280px - Full-width editor windows.</summary>
430+
Xl,
431+
/// <summary>No max-width constraint (fluid).</summary>
432+
Full
433+
}
414434
}

0 commit comments

Comments
 (0)