@object-ui/components 3.0.3 → 3.1.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.
- package/.turbo/turbo-build.log +12 -12
- package/CHANGELOG.md +9 -0
- package/dist/index.css +1 -1
- package/dist/index.js +24932 -23139
- package/dist/index.umd.cjs +37 -37
- package/dist/src/custom/config-field-renderer.d.ts +21 -0
- package/dist/src/custom/config-panel-renderer.d.ts +81 -0
- package/dist/src/custom/config-row.d.ts +27 -0
- package/dist/src/custom/filter-builder.d.ts +1 -1
- package/dist/src/custom/index.d.ts +5 -0
- package/dist/src/custom/mobile-dialog-content.d.ts +20 -0
- package/dist/src/custom/navigation-overlay.d.ts +8 -0
- package/dist/src/custom/section-header.d.ts +31 -0
- package/dist/src/debug/DebugPanel.d.ts +39 -0
- package/dist/src/debug/index.d.ts +9 -0
- package/dist/src/hooks/use-config-draft.d.ts +46 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/renderers/action/action-bar.d.ts +25 -0
- package/dist/src/renderers/action/action-button.d.ts +1 -0
- package/dist/src/types/config-panel.d.ts +92 -0
- package/dist/src/ui/sheet.d.ts +2 -0
- package/dist/src/ui/sidebar.d.ts +4 -0
- package/package.json +17 -17
- package/src/__tests__/__snapshots__/snapshot-critical.test.tsx.snap +3 -3
- package/src/__tests__/action-bar.test.tsx +206 -0
- package/src/__tests__/config-field-renderer.test.tsx +307 -0
- package/src/__tests__/config-panel-renderer.test.tsx +580 -0
- package/src/__tests__/config-primitives.test.tsx +106 -0
- package/src/__tests__/filter-builder.test.tsx +409 -0
- package/src/__tests__/mobile-accessibility.test.tsx +120 -0
- package/src/__tests__/navigation-overlay.test.tsx +97 -0
- package/src/__tests__/use-config-draft.test.tsx +295 -0
- package/src/custom/config-field-renderer.tsx +276 -0
- package/src/custom/config-panel-renderer.tsx +306 -0
- package/src/custom/config-row.tsx +50 -0
- package/src/custom/filter-builder.tsx +76 -25
- package/src/custom/index.ts +5 -0
- package/src/custom/mobile-dialog-content.tsx +67 -0
- package/src/custom/navigation-overlay.tsx +42 -4
- package/src/custom/section-header.tsx +68 -0
- package/src/debug/DebugPanel.tsx +313 -0
- package/src/debug/__tests__/DebugPanel.test.tsx +134 -0
- package/src/debug/index.ts +10 -0
- package/src/hooks/use-config-draft.ts +127 -0
- package/src/index.css +4 -0
- package/src/index.ts +15 -0
- package/src/renderers/action/action-bar.tsx +221 -0
- package/src/renderers/action/action-button.tsx +17 -6
- package/src/renderers/action/index.ts +1 -0
- package/src/renderers/complex/__tests__/data-table-airtable-ux.test.tsx +239 -0
- package/src/renderers/complex/__tests__/data-table.test.ts +16 -0
- package/src/renderers/complex/data-table.tsx +346 -43
- package/src/renderers/data-display/breadcrumb.tsx +3 -2
- package/src/renderers/form/form.tsx +4 -4
- package/src/renderers/navigation/header-bar.tsx +69 -10
- package/src/stories/ConfigPanel.stories.tsx +232 -0
- package/src/types/config-panel.ts +101 -0
- package/src/ui/dialog.tsx +20 -3
- package/src/ui/sheet.tsx +6 -3
- package/src/ui/sidebar.tsx +93 -9
|
@@ -161,12 +161,12 @@ ComponentRegistry.register('form',
|
|
|
161
161
|
};
|
|
162
162
|
|
|
163
163
|
// Determine grid classes based on columns (explicit classes for Tailwind JIT)
|
|
164
|
-
// Mobile-first: 1 column on mobile,
|
|
164
|
+
// Mobile-first: 1 column on mobile, responsive breakpoints for larger screens
|
|
165
165
|
const gridColsClass =
|
|
166
166
|
columns === 1 ? '' :
|
|
167
|
-
columns === 2 ? '
|
|
168
|
-
columns === 3 ? '
|
|
169
|
-
'
|
|
167
|
+
columns === 2 ? 'md:grid-cols-2' :
|
|
168
|
+
columns === 3 ? 'md:grid-cols-2 lg:grid-cols-3' :
|
|
169
|
+
'md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4';
|
|
170
170
|
|
|
171
171
|
const gridClass = columns > 1
|
|
172
172
|
? cn('grid gap-4', gridColsClass)
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
import React from 'react';
|
|
10
10
|
import { ComponentRegistry } from '@object-ui/core';
|
|
11
|
-
import type { HeaderBarSchema } from '@object-ui/types';
|
|
11
|
+
import type { HeaderBarSchema, BreadcrumbItem as BreadcrumbItemType } from '@object-ui/types';
|
|
12
|
+
import { resolveI18nLabel, SchemaRenderer } from '@object-ui/react';
|
|
12
13
|
import {
|
|
13
14
|
SidebarTrigger,
|
|
14
15
|
Separator,
|
|
@@ -17,8 +18,45 @@ import {
|
|
|
17
18
|
BreadcrumbItem,
|
|
18
19
|
BreadcrumbLink,
|
|
19
20
|
BreadcrumbSeparator,
|
|
20
|
-
BreadcrumbPage
|
|
21
|
+
BreadcrumbPage,
|
|
22
|
+
DropdownMenu,
|
|
23
|
+
DropdownMenuTrigger,
|
|
24
|
+
DropdownMenuContent,
|
|
25
|
+
DropdownMenuItem,
|
|
26
|
+
Input,
|
|
21
27
|
} from '../../ui';
|
|
28
|
+
import { ChevronDown, Search } from 'lucide-react';
|
|
29
|
+
|
|
30
|
+
function BreadcrumbLabel({ crumb, isLast }: { crumb: BreadcrumbItemType; isLast: boolean }) {
|
|
31
|
+
const label = resolveI18nLabel(crumb.label) ?? '';
|
|
32
|
+
|
|
33
|
+
if (crumb.siblings && crumb.siblings.length > 0) {
|
|
34
|
+
return (
|
|
35
|
+
<DropdownMenu>
|
|
36
|
+
<DropdownMenuTrigger className="flex items-center gap-1">
|
|
37
|
+
{isLast ? (
|
|
38
|
+
<span className="font-semibold">{label}</span>
|
|
39
|
+
) : (
|
|
40
|
+
<span>{label}</span>
|
|
41
|
+
)}
|
|
42
|
+
<ChevronDown className="h-3 w-3" />
|
|
43
|
+
</DropdownMenuTrigger>
|
|
44
|
+
<DropdownMenuContent align="start">
|
|
45
|
+
{crumb.siblings.map((sibling, i) => (
|
|
46
|
+
<DropdownMenuItem key={i} asChild>
|
|
47
|
+
<a href={sibling.href}>{sibling.label}</a>
|
|
48
|
+
</DropdownMenuItem>
|
|
49
|
+
))}
|
|
50
|
+
</DropdownMenuContent>
|
|
51
|
+
</DropdownMenu>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (isLast) {
|
|
56
|
+
return <BreadcrumbPage>{label}</BreadcrumbPage>;
|
|
57
|
+
}
|
|
58
|
+
return <BreadcrumbLink href={crumb.href || '#'}>{label}</BreadcrumbLink>;
|
|
59
|
+
}
|
|
22
60
|
|
|
23
61
|
ComponentRegistry.register('header-bar',
|
|
24
62
|
({ schema }: { schema: HeaderBarSchema }) => (
|
|
@@ -27,27 +65,48 @@ ComponentRegistry.register('header-bar',
|
|
|
27
65
|
<Separator orientation="vertical" className="mr-2 h-4" />
|
|
28
66
|
<Breadcrumb>
|
|
29
67
|
<BreadcrumbList>
|
|
30
|
-
{schema.crumbs?.map((crumb:
|
|
68
|
+
{schema.crumbs?.map((crumb: BreadcrumbItemType, idx: number) => (
|
|
31
69
|
<React.Fragment key={idx}>
|
|
32
70
|
<BreadcrumbItem>
|
|
33
|
-
{idx === schema.crumbs
|
|
34
|
-
<BreadcrumbPage>{crumb.label}</BreadcrumbPage>
|
|
35
|
-
) : (
|
|
36
|
-
<BreadcrumbLink href={crumb.href || '#'}>{crumb.label}</BreadcrumbLink>
|
|
37
|
-
)}
|
|
71
|
+
<BreadcrumbLabel crumb={crumb} isLast={idx === schema.crumbs!.length - 1} />
|
|
38
72
|
</BreadcrumbItem>
|
|
39
|
-
{idx < schema.crumbs
|
|
73
|
+
{idx < schema.crumbs!.length - 1 && <BreadcrumbSeparator />}
|
|
40
74
|
</React.Fragment>
|
|
41
75
|
))}
|
|
42
76
|
</BreadcrumbList>
|
|
43
77
|
</Breadcrumb>
|
|
78
|
+
|
|
79
|
+
<div className="ml-auto flex items-center gap-2">
|
|
80
|
+
{schema.search?.enabled && (
|
|
81
|
+
<div className="relative">
|
|
82
|
+
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
83
|
+
<Input
|
|
84
|
+
type="search"
|
|
85
|
+
placeholder={schema.search.placeholder}
|
|
86
|
+
className="pl-8 w-[200px] lg:w-[300px]"
|
|
87
|
+
/>
|
|
88
|
+
{schema.search.shortcut && (
|
|
89
|
+
<kbd className="pointer-events-none absolute right-2 top-2 hidden h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium opacity-100 sm:flex">
|
|
90
|
+
{schema.search.shortcut}
|
|
91
|
+
</kbd>
|
|
92
|
+
)}
|
|
93
|
+
</div>
|
|
94
|
+
)}
|
|
95
|
+
{schema.actions?.map((action, idx) => (
|
|
96
|
+
<SchemaRenderer key={idx} schema={action} />
|
|
97
|
+
))}
|
|
98
|
+
{schema.rightContent && <SchemaRenderer schema={schema.rightContent} />}
|
|
99
|
+
</div>
|
|
44
100
|
</header>
|
|
45
101
|
),
|
|
46
102
|
{
|
|
47
103
|
namespace: 'ui',
|
|
48
104
|
label: 'Header Bar',
|
|
49
105
|
inputs: [
|
|
50
|
-
{ name: 'crumbs', type: 'array', label: 'Breadcrumbs' }
|
|
106
|
+
{ name: 'crumbs', type: 'array', label: 'Breadcrumbs' },
|
|
107
|
+
{ name: 'search', type: 'object', label: 'Search Configuration' },
|
|
108
|
+
{ name: 'actions', type: 'array', label: 'Action Slots' },
|
|
109
|
+
{ name: 'rightContent', type: 'object', label: 'Right Content' },
|
|
51
110
|
],
|
|
52
111
|
defaultProps: {
|
|
53
112
|
crumbs: [
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React, { useState } from 'react';
|
|
10
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
11
|
+
import { ConfigPanelRenderer } from '../custom/config-panel-renderer';
|
|
12
|
+
import { ConfigFieldRenderer } from '../custom/config-field-renderer';
|
|
13
|
+
import { useConfigDraft } from '../hooks/use-config-draft';
|
|
14
|
+
import type { ConfigPanelSchema, ConfigField } from '../types/config-panel';
|
|
15
|
+
|
|
16
|
+
// ─── ConfigPanelRenderer Stories ─────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
const panelMeta = {
|
|
19
|
+
title: 'Config/ConfigPanelRenderer',
|
|
20
|
+
component: ConfigPanelRenderer,
|
|
21
|
+
parameters: {
|
|
22
|
+
layout: 'padded',
|
|
23
|
+
},
|
|
24
|
+
tags: ['autodocs'],
|
|
25
|
+
} satisfies Meta<typeof ConfigPanelRenderer>;
|
|
26
|
+
|
|
27
|
+
export default panelMeta;
|
|
28
|
+
type Story = StoryObj<typeof panelMeta>;
|
|
29
|
+
|
|
30
|
+
// Basic schema example
|
|
31
|
+
const basicSchema: ConfigPanelSchema = {
|
|
32
|
+
breadcrumb: ['Settings', 'General'],
|
|
33
|
+
sections: [
|
|
34
|
+
{
|
|
35
|
+
key: 'basic',
|
|
36
|
+
title: 'Basic',
|
|
37
|
+
fields: [
|
|
38
|
+
{ key: 'title', label: 'Title', type: 'input', placeholder: 'Enter title' },
|
|
39
|
+
{ key: 'enabled', label: 'Enabled', type: 'switch', defaultValue: true },
|
|
40
|
+
{
|
|
41
|
+
key: 'mode',
|
|
42
|
+
label: 'Mode',
|
|
43
|
+
type: 'select',
|
|
44
|
+
options: [
|
|
45
|
+
{ value: 'auto', label: 'Auto' },
|
|
46
|
+
{ value: 'manual', label: 'Manual' },
|
|
47
|
+
{ value: 'scheduled', label: 'Scheduled' },
|
|
48
|
+
],
|
|
49
|
+
defaultValue: 'auto',
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
key: 'appearance',
|
|
55
|
+
title: 'Appearance',
|
|
56
|
+
collapsible: true,
|
|
57
|
+
fields: [
|
|
58
|
+
{ key: 'theme', label: 'Theme', type: 'select', options: [
|
|
59
|
+
{ value: 'light', label: 'Light' },
|
|
60
|
+
{ value: 'dark', label: 'Dark' },
|
|
61
|
+
{ value: 'system', label: 'System' },
|
|
62
|
+
], defaultValue: 'system' },
|
|
63
|
+
{ key: 'accentColor', label: 'Accent color', type: 'color', defaultValue: '#3b82f6' },
|
|
64
|
+
{ key: 'fontSize', label: 'Font size', type: 'slider', min: 10, max: 24, step: 1, defaultValue: 14 },
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
key: 'advanced',
|
|
69
|
+
title: 'Advanced',
|
|
70
|
+
collapsible: true,
|
|
71
|
+
defaultCollapsed: true,
|
|
72
|
+
hint: 'These settings are for power users.',
|
|
73
|
+
fields: [
|
|
74
|
+
{ key: 'debug', label: 'Debug mode', type: 'checkbox' },
|
|
75
|
+
{ key: 'apiEndpoint', label: 'API endpoint', type: 'input', placeholder: 'https://...' },
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
function BasicPanelStory() {
|
|
82
|
+
const source = { title: 'My App', enabled: true, mode: 'auto', theme: 'system', accentColor: '#3b82f6', fontSize: 14, debug: false, apiEndpoint: '' };
|
|
83
|
+
const { draft, isDirty, updateField, discard } = useConfigDraft(source);
|
|
84
|
+
return (
|
|
85
|
+
<div style={{ position: 'relative', height: 600, width: 320, border: '1px solid #e5e7eb', borderRadius: 8, overflow: 'hidden' }}>
|
|
86
|
+
<ConfigPanelRenderer
|
|
87
|
+
open={true}
|
|
88
|
+
onClose={() => alert('Close clicked')}
|
|
89
|
+
schema={basicSchema}
|
|
90
|
+
draft={draft}
|
|
91
|
+
isDirty={isDirty}
|
|
92
|
+
onFieldChange={updateField}
|
|
93
|
+
onSave={() => alert('Saved: ' + JSON.stringify(draft, null, 2))}
|
|
94
|
+
onDiscard={discard}
|
|
95
|
+
/>
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export const Basic: Story = {
|
|
101
|
+
render: () => <BasicPanelStory />,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// Dashboard-like schema
|
|
105
|
+
const dashboardSchema: ConfigPanelSchema = {
|
|
106
|
+
breadcrumb: ['Dashboard', 'Layout'],
|
|
107
|
+
sections: [
|
|
108
|
+
{
|
|
109
|
+
key: 'layout',
|
|
110
|
+
title: 'Layout',
|
|
111
|
+
fields: [
|
|
112
|
+
{ key: 'columns', label: 'Columns', type: 'slider', min: 1, max: 12, step: 1, defaultValue: 3 },
|
|
113
|
+
{ key: 'gap', label: 'Gap', type: 'slider', min: 0, max: 16, step: 1, defaultValue: 4 },
|
|
114
|
+
{ key: 'rowHeight', label: 'Row height', type: 'input', defaultValue: '120', placeholder: 'px' },
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
key: 'data',
|
|
119
|
+
title: 'Data',
|
|
120
|
+
collapsible: true,
|
|
121
|
+
fields: [
|
|
122
|
+
{ key: 'refreshInterval', label: 'Refresh interval', type: 'select', options: [
|
|
123
|
+
{ value: '0', label: 'Manual' },
|
|
124
|
+
{ value: '30', label: '30s' },
|
|
125
|
+
{ value: '60', label: '1 min' },
|
|
126
|
+
{ value: '300', label: '5 min' },
|
|
127
|
+
], defaultValue: '0' },
|
|
128
|
+
{ key: 'autoRefresh', label: 'Auto-refresh', type: 'switch', defaultValue: false },
|
|
129
|
+
],
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
key: 'appearance',
|
|
133
|
+
title: 'Appearance',
|
|
134
|
+
collapsible: true,
|
|
135
|
+
defaultCollapsed: true,
|
|
136
|
+
fields: [
|
|
137
|
+
{ key: 'showTitle', label: 'Show title', type: 'switch', defaultValue: true },
|
|
138
|
+
{ key: 'showDescription', label: 'Show description', type: 'switch', defaultValue: true },
|
|
139
|
+
{ key: 'theme', label: 'Theme', type: 'select', options: [
|
|
140
|
+
{ value: 'light', label: 'Light' },
|
|
141
|
+
{ value: 'dark', label: 'Dark' },
|
|
142
|
+
{ value: 'auto', label: 'Auto' },
|
|
143
|
+
], defaultValue: 'auto' },
|
|
144
|
+
],
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
function DashboardPanelStory() {
|
|
150
|
+
const source = { columns: 3, gap: 4, rowHeight: '120', refreshInterval: '0', autoRefresh: false, showTitle: true, showDescription: true, theme: 'auto' };
|
|
151
|
+
const { draft, isDirty, updateField, discard } = useConfigDraft(source);
|
|
152
|
+
return (
|
|
153
|
+
<div style={{ position: 'relative', height: 600, width: 320, border: '1px solid #e5e7eb', borderRadius: 8, overflow: 'hidden' }}>
|
|
154
|
+
<ConfigPanelRenderer
|
|
155
|
+
open={true}
|
|
156
|
+
onClose={() => alert('Close')}
|
|
157
|
+
schema={dashboardSchema}
|
|
158
|
+
draft={draft}
|
|
159
|
+
isDirty={isDirty}
|
|
160
|
+
onFieldChange={updateField}
|
|
161
|
+
onSave={() => alert('Saved!')}
|
|
162
|
+
onDiscard={discard}
|
|
163
|
+
/>
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export const DashboardConfig: Story = {
|
|
169
|
+
render: () => <DashboardPanelStory />,
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// Custom field escape hatch
|
|
173
|
+
const customSchema: ConfigPanelSchema = {
|
|
174
|
+
breadcrumb: ['View', 'Config'],
|
|
175
|
+
sections: [
|
|
176
|
+
{
|
|
177
|
+
key: 'main',
|
|
178
|
+
title: 'General',
|
|
179
|
+
fields: [
|
|
180
|
+
{ key: 'name', label: 'View name', type: 'input' },
|
|
181
|
+
{
|
|
182
|
+
key: 'rowHeight',
|
|
183
|
+
label: 'Row height',
|
|
184
|
+
type: 'custom',
|
|
185
|
+
render: (value, onChange) => (
|
|
186
|
+
<div style={{ display: 'flex', gap: 4, padding: '4px 0' }}>
|
|
187
|
+
{['compact', 'medium', 'tall'].map((h) => (
|
|
188
|
+
<button
|
|
189
|
+
key={h}
|
|
190
|
+
onClick={() => onChange(h)}
|
|
191
|
+
style={{
|
|
192
|
+
padding: '2px 8px',
|
|
193
|
+
fontSize: 11,
|
|
194
|
+
borderRadius: 4,
|
|
195
|
+
border: value === h ? '2px solid #3b82f6' : '1px solid #d1d5db',
|
|
196
|
+
background: value === h ? '#eff6ff' : 'transparent',
|
|
197
|
+
cursor: 'pointer',
|
|
198
|
+
}}
|
|
199
|
+
>
|
|
200
|
+
{h}
|
|
201
|
+
</button>
|
|
202
|
+
))}
|
|
203
|
+
</div>
|
|
204
|
+
),
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
function CustomFieldStory() {
|
|
212
|
+
const source = { name: 'My Grid View', rowHeight: 'medium' };
|
|
213
|
+
const { draft, isDirty, updateField, discard } = useConfigDraft(source);
|
|
214
|
+
return (
|
|
215
|
+
<div style={{ position: 'relative', height: 400, width: 320, border: '1px solid #e5e7eb', borderRadius: 8, overflow: 'hidden' }}>
|
|
216
|
+
<ConfigPanelRenderer
|
|
217
|
+
open={true}
|
|
218
|
+
onClose={() => {}}
|
|
219
|
+
schema={customSchema}
|
|
220
|
+
draft={draft}
|
|
221
|
+
isDirty={isDirty}
|
|
222
|
+
onFieldChange={updateField}
|
|
223
|
+
onSave={() => alert('Saved!')}
|
|
224
|
+
onDiscard={discard}
|
|
225
|
+
/>
|
|
226
|
+
</div>
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export const CustomFieldEscapeHatch: Story = {
|
|
231
|
+
render: () => <CustomFieldStory />,
|
|
232
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Schema-driven config panel type definitions.
|
|
11
|
+
*
|
|
12
|
+
* Each concrete panel (View, Dashboard, Page…) provides a ConfigPanelSchema,
|
|
13
|
+
* and ConfigPanelRenderer auto-generates the UI.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Supported control types for config fields */
|
|
17
|
+
export type ControlType =
|
|
18
|
+
| 'input'
|
|
19
|
+
| 'switch'
|
|
20
|
+
| 'select'
|
|
21
|
+
| 'checkbox'
|
|
22
|
+
| 'slider'
|
|
23
|
+
| 'color'
|
|
24
|
+
| 'field-picker'
|
|
25
|
+
| 'filter'
|
|
26
|
+
| 'sort'
|
|
27
|
+
| 'icon-group'
|
|
28
|
+
| 'summary'
|
|
29
|
+
| 'custom';
|
|
30
|
+
|
|
31
|
+
/** A single field within a config section */
|
|
32
|
+
export interface ConfigField {
|
|
33
|
+
/** Field key in the draft object */
|
|
34
|
+
key: string;
|
|
35
|
+
/** Display label (may be an i18n key) */
|
|
36
|
+
label: string;
|
|
37
|
+
/** Control type determining which widget to render */
|
|
38
|
+
type: ControlType;
|
|
39
|
+
/** Default value for the field */
|
|
40
|
+
defaultValue?: any;
|
|
41
|
+
/** Select/icon-group options */
|
|
42
|
+
options?: Array<{ value: string; label: string; icon?: React.ReactNode }>;
|
|
43
|
+
/** Visibility predicate evaluated against the current draft */
|
|
44
|
+
visibleWhen?: (draft: Record<string, any>) => boolean;
|
|
45
|
+
/** Disabled predicate evaluated against the current draft */
|
|
46
|
+
disabledWhen?: (draft: Record<string, any>) => boolean;
|
|
47
|
+
/** Custom render function for type='custom' */
|
|
48
|
+
render?: (
|
|
49
|
+
value: any,
|
|
50
|
+
onChange: (v: any) => void,
|
|
51
|
+
draft: Record<string, any>,
|
|
52
|
+
) => React.ReactNode;
|
|
53
|
+
/** Placeholder text for input/select controls */
|
|
54
|
+
placeholder?: string;
|
|
55
|
+
/** Help text displayed below the control */
|
|
56
|
+
helpText?: string;
|
|
57
|
+
/** Minimum value for slider */
|
|
58
|
+
min?: number;
|
|
59
|
+
/** Maximum value for slider */
|
|
60
|
+
max?: number;
|
|
61
|
+
/** Step value for slider */
|
|
62
|
+
step?: number;
|
|
63
|
+
/** Whether the field is disabled */
|
|
64
|
+
disabled?: boolean;
|
|
65
|
+
/** Field definitions for filter/sort sub-editors */
|
|
66
|
+
fields?: Array<{ value: string; label: string; type?: string; options?: Array<{ value: string; label: string }> }>;
|
|
67
|
+
/** Summary display text for type='summary' */
|
|
68
|
+
summaryText?: string;
|
|
69
|
+
/** Click handler for the summary gear/action button (type='summary') */
|
|
70
|
+
onSummaryClick?: () => void;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** A group of related config fields */
|
|
74
|
+
export interface ConfigSection {
|
|
75
|
+
/** Unique section key (used for collapse state tracking) */
|
|
76
|
+
key: string;
|
|
77
|
+
/** Section title (may be an i18n key) */
|
|
78
|
+
title: string;
|
|
79
|
+
/** Hint text displayed below the title */
|
|
80
|
+
hint?: string;
|
|
81
|
+
/** Icon displayed before the section title (e.g. a Lucide icon element) */
|
|
82
|
+
icon?: React.ReactNode;
|
|
83
|
+
/** Whether this section supports collapse/expand */
|
|
84
|
+
collapsible?: boolean;
|
|
85
|
+
/** Default collapsed state */
|
|
86
|
+
defaultCollapsed?: boolean;
|
|
87
|
+
/** Fields belonging to this section */
|
|
88
|
+
fields: ConfigField[];
|
|
89
|
+
/** Nested sub-sections for complex grouping */
|
|
90
|
+
subsections?: ConfigSection[];
|
|
91
|
+
/** Visibility predicate evaluated against the current draft */
|
|
92
|
+
visibleWhen?: (draft: Record<string, any>) => boolean;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Top-level schema describing an entire config panel */
|
|
96
|
+
export interface ConfigPanelSchema {
|
|
97
|
+
/** Breadcrumb segments displayed in the panel header */
|
|
98
|
+
breadcrumb: string[];
|
|
99
|
+
/** Ordered list of sections */
|
|
100
|
+
sections: ConfigSection[];
|
|
101
|
+
}
|
package/src/ui/dialog.tsx
CHANGED
|
@@ -46,14 +46,31 @@ const DialogContent = React.forwardRef<
|
|
|
46
46
|
<DialogPrimitive.Content
|
|
47
47
|
ref={ref}
|
|
48
48
|
className={cn(
|
|
49
|
-
|
|
49
|
+
// Mobile-first: full-screen
|
|
50
|
+
"fixed inset-0 z-50 grid w-full bg-background p-4 shadow-lg duration-200",
|
|
51
|
+
"h-[100dvh]",
|
|
52
|
+
// Desktop (sm+): centered dialog with border + rounded corners
|
|
53
|
+
"sm:inset-auto sm:left-[50%] sm:top-[50%] sm:translate-x-[-50%] sm:translate-y-[-50%]",
|
|
54
|
+
"sm:max-w-lg sm:h-auto sm:max-h-[90vh] sm:rounded-lg sm:border sm:p-6",
|
|
55
|
+
// Animations
|
|
56
|
+
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
|
57
|
+
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
58
|
+
"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
|
|
59
|
+
"data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%]",
|
|
60
|
+
"data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]",
|
|
50
61
|
className
|
|
51
62
|
)}
|
|
52
63
|
{...props}
|
|
53
64
|
>
|
|
54
65
|
{children}
|
|
55
|
-
<DialogPrimitive.Close className=
|
|
56
|
-
|
|
66
|
+
<DialogPrimitive.Close className={cn(
|
|
67
|
+
"absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity",
|
|
68
|
+
"hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
|
69
|
+
"disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground",
|
|
70
|
+
// Mobile touch target ≥ 44×44px (WCAG 2.5.5)
|
|
71
|
+
"min-h-[44px] min-w-[44px] sm:min-h-0 sm:min-w-0 flex items-center justify-center",
|
|
72
|
+
)}>
|
|
73
|
+
<X className="h-5 w-5 sm:h-4 sm:w-4" />
|
|
57
74
|
<span className="sr-only">Close</span>
|
|
58
75
|
</DialogPrimitive.Close>
|
|
59
76
|
</DialogPrimitive.Content>
|
package/src/ui/sheet.tsx
CHANGED
|
@@ -59,14 +59,17 @@ const sheetVariants = cva(
|
|
|
59
59
|
|
|
60
60
|
interface SheetContentProps
|
|
61
61
|
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
|
62
|
-
VariantProps<typeof sheetVariants> {
|
|
62
|
+
VariantProps<typeof sheetVariants> {
|
|
63
|
+
/** When true, the backdrop overlay is not rendered (useful for non-modal drawers) */
|
|
64
|
+
hideOverlay?: boolean;
|
|
65
|
+
}
|
|
63
66
|
|
|
64
67
|
const SheetContent = React.forwardRef<
|
|
65
68
|
React.ElementRef<typeof SheetPrimitive.Content>,
|
|
66
69
|
SheetContentProps
|
|
67
|
-
>(({ side = "right", className, children, ...props }, ref) => (
|
|
70
|
+
>(({ side = "right", className, children, hideOverlay, ...props }, ref) => (
|
|
68
71
|
<SheetPortal>
|
|
69
|
-
<SheetOverlay />
|
|
72
|
+
{!hideOverlay && <SheetOverlay />}
|
|
70
73
|
<SheetPrimitive.Content
|
|
71
74
|
ref={ref}
|
|
72
75
|
className={cn(sheetVariants({ side }), className)}
|