@pablozaiden/webapp 0.6.8 → 1.0.0
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/docs/settings.md +11 -0
- package/docs/ui-guidelines.md +5 -2
- package/package.json +1 -1
- package/src/web/WebAppRoot.tsx +8 -1
- package/src/web/components/index.tsx +87 -14
- package/src/web/mobile-hooks.ts +36 -2
- package/src/web/root-types.ts +3 -0
- package/src/web/settings/security-section.tsx +22 -18
- package/src/web/settings/settings-view.tsx +5 -3
- package/src/web/styles.css +163 -1
package/docs/settings.md
CHANGED
|
@@ -56,6 +56,12 @@ Apps can append structured custom sections:
|
|
|
56
56
|
description: lastSync,
|
|
57
57
|
actions: [{ id: "sync-now", label: "Sync now", onAction: syncNow }],
|
|
58
58
|
},
|
|
59
|
+
{
|
|
60
|
+
id: "auto-sync",
|
|
61
|
+
title: "Automatic sync",
|
|
62
|
+
content: <input type="checkbox" checked={autoSync} onChange={toggleAutoSync} />,
|
|
63
|
+
contentPlacement: "inline",
|
|
64
|
+
},
|
|
59
65
|
{
|
|
60
66
|
id: "disconnect",
|
|
61
67
|
title: "Disconnect",
|
|
@@ -73,4 +79,9 @@ Apps can append structured custom sections:
|
|
|
73
79
|
|
|
74
80
|
`render` remains available as an escape hatch for custom controls inside a section. Prefer structured `rows` for simple settings because the framework keeps spacing, typography and danger-zone styling consistent.
|
|
75
81
|
|
|
82
|
+
Row content is placed below the title and description by default. Use
|
|
83
|
+
`contentPlacement: "inline"` for compact controls such as a checkbox or toggle
|
|
84
|
+
that should remain on the right side of the row. Keep selects and larger
|
|
85
|
+
controls at the default `"below"` placement.
|
|
86
|
+
|
|
76
87
|
`scope` can be `user`, `admin` or `owner` on both sections and rows. Omitted scope behaves like `user`. Use `admin` for global app/server settings and `user` for preferences or data that belong to the signed-in user.
|
package/docs/ui-guidelines.md
CHANGED
|
@@ -29,7 +29,7 @@ twice without a distinct user-facing reason.
|
|
|
29
29
|
|
|
30
30
|
The framework owns the mobile shell breakpoint. `MOBILE_BREAKPOINT_PX` and `MOBILE_MEDIA_QUERY` are exported from `@pablozaiden/webapp/web` for application JavaScript that needs to coordinate with the shell; do not add an independent `innerWidth` threshold for shell behavior. The generated document initializes the `data-wapp-mobile` marker on the root element before the client and styles load, and `WebAppRoot` keeps it synchronized with media-query changes. Custom CSS that follows the framework mobile mode should use that marker rather than repeating a numeric media query.
|
|
31
31
|
|
|
32
|
-
`WebAppRoot` follows `visualViewport` resize/scroll, window resize, orientation changes, focus boundaries, and mobile media-query changes. CSS `100dvh` is the authoritative height when the browser supports dynamic viewport units
|
|
32
|
+
`WebAppRoot` follows `visualViewport` resize/scroll, window resize, orientation changes, focus boundaries, and mobile media-query changes. CSS `100dvh` is the authoritative height when the browser supports dynamic viewport units, except while an editable control is focused and the visual viewport is materially shorter because the on-screen keyboard is open. In that keyboard state, the hook temporarily overrides the shell height with the measured visual viewport so the keyboard cannot cover the app. The visual-viewport pixel value remains the normal fallback for browsers without `100dvh`. Two named, bounded retries catch the post-event layout pass and the end of keyboard, browser-chrome, or rotation transitions when no further geometry event is emitted. These retries are implementation fallbacks, not general synchronization primitives, and are cancelled with the associated animation frame and listeners when the root is unmounted.
|
|
33
33
|
|
|
34
34
|
The framework mobile mode is separate from the narrower `640px` settings-layout rules. Do not add global touch handlers or arbitrary sleeps to reproduce either behavior; use the framework drawer controls and the existing event-driven viewport lifecycle.
|
|
35
35
|
|
|
@@ -44,7 +44,8 @@ Use these first:
|
|
|
44
44
|
| `Panel` | Cards/sections; use `actions` for a top-right menu/action area |
|
|
45
45
|
| `ActionMenu` | Three-line action menu for secondary surfaces; entity-level shell menus should usually come from `SidebarNode.actions` so the framework renders them in the sidebar context menu and fixed title bar |
|
|
46
46
|
| `Button` / `IconButton` | Form submission and true inline controls; prefer action menus for entity/app commands |
|
|
47
|
-
| `Badge` |
|
|
47
|
+
| `Badge` | Generic status/count labels; preserves the supplied text casing |
|
|
48
|
+
| `StatusBadge` | Status labels with the shared uppercase and letter-spacing treatment |
|
|
48
49
|
| `EntityHeader` | Main-content entity title/description/actions |
|
|
49
50
|
| `DataList` / `DataListRow` | Lists with title, description, metadata, badge and actions |
|
|
50
51
|
| `TextField`, `TextAreaField`, `SelectField` | Forms |
|
|
@@ -56,6 +57,8 @@ Use these first:
|
|
|
56
57
|
| `EmptyState` | Empty or missing content |
|
|
57
58
|
| `ConfirmDialog` | Destructive confirmation |
|
|
58
59
|
|
|
60
|
+
The default `Panel` is the muted card surface, `DataList` renders card rows, and `Button` uses the primary medium-sized action treatment. Use an explicit variant or size only when the surface is intentionally different from the application baseline.
|
|
61
|
+
|
|
59
62
|
For entity actions, prefer the framework title bar: define one `ActionMenuItem[]` builder and attach it to `SidebarNode.actions` for the route-backed node. The framework reuses those actions for both sidebar right-click and the active route title-bar menu. Use `WebAppRoot.header.getActions` only for additional actions not owned by a sidebar node.
|
|
60
63
|
|
|
61
64
|
Every route component rendered by `WebAppRoot.routes` should return `Page` at the top level, including loading/error/empty states. Do not render a `Panel`, `DataList`, `EmptyState` or custom div directly into `WebAppRoot`; that skips the standard content margins and recreates the visual bug where cards touch the main content edge. For a route whose child owns viewport-sized layout and scrolling, use `<Page layout="full">` rather than overriding `.wapp-page` from the application. Use `EntityHeader` only when the page needs a content-specific heading distinct from the fixed framework title bar; do not duplicate the active route title immediately below the header.
|
package/package.json
CHANGED
package/src/web/WebAppRoot.tsx
CHANGED
|
@@ -13,7 +13,14 @@ import { ThemeProvider } from "./theme";
|
|
|
13
13
|
import { WebAppConfigProvider, useWebAppConfig } from "./webapp-config";
|
|
14
14
|
|
|
15
15
|
export { replaceHashRoute, replaceWebAppRoute, routeToHash } from "./routing";
|
|
16
|
-
export type {
|
|
16
|
+
export type {
|
|
17
|
+
HeaderContext,
|
|
18
|
+
SettingsAction,
|
|
19
|
+
SettingsRow,
|
|
20
|
+
SettingsRowContentPlacement,
|
|
21
|
+
SettingsSection,
|
|
22
|
+
WebAppRootProps,
|
|
23
|
+
} from "./root-types";
|
|
17
24
|
|
|
18
25
|
function routeMatches(left: WebAppRoute | undefined, right: WebAppRoute): boolean {
|
|
19
26
|
if (!left) {
|
|
@@ -2,16 +2,20 @@ import { useCallback, useEffect, useId, useLayoutEffect, useRef, useState, type
|
|
|
2
2
|
import { createPortal } from "react-dom";
|
|
3
3
|
import type { ActionMenuItem, BadgeVariant } from "../sidebar/types";
|
|
4
4
|
|
|
5
|
+
export type ButtonVariant = "default" | "primary" | "danger" | "ghost";
|
|
6
|
+
export type ButtonSize = "xs" | "sm" | "md" | "lg";
|
|
7
|
+
|
|
5
8
|
export function Button({
|
|
6
|
-
variant = "
|
|
9
|
+
variant = "primary",
|
|
10
|
+
size = "md",
|
|
7
11
|
loading = false,
|
|
8
12
|
className = "",
|
|
9
13
|
disabled,
|
|
10
14
|
children,
|
|
11
15
|
...props
|
|
12
|
-
}: ButtonHTMLAttributes<HTMLButtonElement> & { variant?:
|
|
16
|
+
}: ButtonHTMLAttributes<HTMLButtonElement> & { variant?: ButtonVariant; size?: ButtonSize; loading?: boolean }) {
|
|
13
17
|
return (
|
|
14
|
-
<button {...props} disabled={disabled || loading} className={`wapp-button wapp-button-${variant} ${className}`}>
|
|
18
|
+
<button {...props} disabled={disabled || loading} className={`wapp-button wapp-button-${variant} wapp-button-${size} ${className}`}>
|
|
15
19
|
{loading ? <span className="wapp-button-spinner" aria-hidden="true" /> : null}
|
|
16
20
|
{children}
|
|
17
21
|
</button>
|
|
@@ -26,8 +30,27 @@ export function IconButton({
|
|
|
26
30
|
return <button {...props} className={`wapp-icon-button ${active ? "active" : ""} ${className}`} />;
|
|
27
31
|
}
|
|
28
32
|
|
|
29
|
-
export
|
|
30
|
-
|
|
33
|
+
export type BadgeSize = "sm" | "md";
|
|
34
|
+
|
|
35
|
+
export interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
|
|
36
|
+
variant?: BadgeVariant;
|
|
37
|
+
size?: BadgeSize;
|
|
38
|
+
children: ReactNode;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function Badge({
|
|
42
|
+
variant = "default",
|
|
43
|
+
size = "sm",
|
|
44
|
+
className = "",
|
|
45
|
+
children,
|
|
46
|
+
...props
|
|
47
|
+
}: BadgeProps) {
|
|
48
|
+
const sizeClass = size === "md" ? "wapp-badge-md" : "";
|
|
49
|
+
return <span {...props} className={["wapp-badge", `wapp-badge-${variant}`, sizeClass, className].filter(Boolean).join(" ")}>{children}</span>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function StatusBadge({ className = "", ...props }: BadgeProps) {
|
|
53
|
+
return <Badge {...props} className={["wapp-status-badge", className].filter(Boolean).join(" ")} />;
|
|
31
54
|
}
|
|
32
55
|
|
|
33
56
|
export type PageLayout = "padded" | "full";
|
|
@@ -41,9 +64,31 @@ export function Page({
|
|
|
41
64
|
return <div {...props} className={`wapp-page ${layout === "full" ? "wapp-page-full" : ""} ${className}`.trim()}>{children}</div>;
|
|
42
65
|
}
|
|
43
66
|
|
|
44
|
-
export
|
|
67
|
+
export type PanelVariant = "surface" | "muted" | "plain";
|
|
68
|
+
export type PanelPadding = "default" | "compact" | "none";
|
|
69
|
+
|
|
70
|
+
export interface PanelProps extends HTMLAttributes<HTMLElement> {
|
|
71
|
+
title?: string;
|
|
72
|
+
description?: string;
|
|
73
|
+
actions?: ReactNode;
|
|
74
|
+
variant?: PanelVariant;
|
|
75
|
+
padding?: PanelPadding;
|
|
76
|
+
children?: ReactNode;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function Panel({
|
|
80
|
+
title,
|
|
81
|
+
description,
|
|
82
|
+
actions,
|
|
83
|
+
children,
|
|
84
|
+
variant = "muted",
|
|
85
|
+
padding = "default",
|
|
86
|
+
className = "",
|
|
87
|
+
...props
|
|
88
|
+
}: PanelProps) {
|
|
89
|
+
const paddingClass = padding === "default" ? "" : `wapp-panel-padding-${padding}`;
|
|
45
90
|
return (
|
|
46
|
-
<section className={`wapp-panel ${className}
|
|
91
|
+
<section {...props} className={`wapp-panel wapp-panel-${variant} ${paddingClass} ${className}`.trim()}>
|
|
47
92
|
{title || description || actions ? (
|
|
48
93
|
<div className="wapp-panel-header">
|
|
49
94
|
<div>
|
|
@@ -111,40 +156,68 @@ export function EntityHeader({
|
|
|
111
156
|
);
|
|
112
157
|
}
|
|
113
158
|
|
|
114
|
-
export
|
|
115
|
-
|
|
159
|
+
export type DataListVariant = "divided" | "cards";
|
|
160
|
+
|
|
161
|
+
export function DataList({
|
|
162
|
+
children,
|
|
163
|
+
empty,
|
|
164
|
+
variant = "cards",
|
|
165
|
+
}: {
|
|
166
|
+
children?: ReactNode;
|
|
167
|
+
empty?: ReactNode;
|
|
168
|
+
variant?: DataListVariant;
|
|
169
|
+
}) {
|
|
170
|
+
return <div className={`wapp-data-list wapp-data-list-${variant}`}>{children ?? empty ?? null}</div>;
|
|
116
171
|
}
|
|
117
172
|
|
|
118
173
|
export function DataListRow({
|
|
119
174
|
title,
|
|
120
175
|
description,
|
|
176
|
+
descriptionClassName = "",
|
|
121
177
|
meta,
|
|
178
|
+
metaPlacement = "side",
|
|
122
179
|
badge,
|
|
123
180
|
actions,
|
|
124
181
|
onClick,
|
|
182
|
+
disabled = false,
|
|
183
|
+
variant = "card",
|
|
184
|
+
className = "",
|
|
125
185
|
}: {
|
|
126
186
|
title: ReactNode;
|
|
127
187
|
description?: ReactNode;
|
|
188
|
+
descriptionClassName?: string;
|
|
128
189
|
meta?: ReactNode;
|
|
190
|
+
metaPlacement?: "side" | "below";
|
|
129
191
|
badge?: ReactNode;
|
|
130
192
|
actions?: ReactNode;
|
|
131
193
|
onClick?: () => void;
|
|
194
|
+
disabled?: boolean;
|
|
195
|
+
variant?: "default" | "card";
|
|
196
|
+
className?: string;
|
|
132
197
|
}) {
|
|
198
|
+
const rowClassName = [
|
|
199
|
+
"wapp-data-list-row",
|
|
200
|
+
variant === "card" ? "wapp-data-list-row-card" : "",
|
|
201
|
+
onClick && !disabled ? "interactive" : "",
|
|
202
|
+
disabled ? "disabled" : "",
|
|
203
|
+
className,
|
|
204
|
+
].filter(Boolean).join(" ");
|
|
133
205
|
const content = (
|
|
134
206
|
<>
|
|
135
207
|
<span className="wapp-data-list-row-main">
|
|
136
208
|
<strong>{title}</strong>
|
|
137
|
-
{description ? <small>{description}</small> : null}
|
|
209
|
+
{description ? <small className={descriptionClassName}>{description}</small> : null}
|
|
210
|
+
{meta && metaPlacement === "below" ? <small className="wapp-data-list-row-meta-below">{meta}</small> : null}
|
|
138
211
|
</span>
|
|
139
|
-
{meta ? <span className="wapp-data-list-row-meta">{meta}</span> : null}
|
|
212
|
+
{meta && metaPlacement === "side" ? <span className="wapp-data-list-row-meta">{meta}</span> : null}
|
|
140
213
|
{badge ? <span className="wapp-data-list-row-badge">{badge}</span> : null}
|
|
141
214
|
{actions ? <span className="wapp-data-list-row-actions">{actions}</span> : null}
|
|
142
215
|
</>
|
|
143
216
|
);
|
|
144
|
-
return onClick ? (
|
|
145
|
-
<button type="button" className=
|
|
217
|
+
return onClick && !disabled ? (
|
|
218
|
+
<button type="button" className={rowClassName} onClick={onClick}>{content}</button>
|
|
146
219
|
) : (
|
|
147
|
-
<div className=
|
|
220
|
+
<div className={rowClassName}>{content}</div>
|
|
148
221
|
);
|
|
149
222
|
}
|
|
150
223
|
|
package/src/web/mobile-hooks.ts
CHANGED
|
@@ -6,6 +6,32 @@ import {
|
|
|
6
6
|
MOBILE_VIEWPORT_FIRST_SETTLE_DELAY_MS,
|
|
7
7
|
} from "./mobile";
|
|
8
8
|
|
|
9
|
+
const NON_TEXT_INPUT_TYPES = new Set([
|
|
10
|
+
"button",
|
|
11
|
+
"checkbox",
|
|
12
|
+
"color",
|
|
13
|
+
"file",
|
|
14
|
+
"hidden",
|
|
15
|
+
"image",
|
|
16
|
+
"radio",
|
|
17
|
+
"range",
|
|
18
|
+
"reset",
|
|
19
|
+
"submit",
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
function isEditableElement(element: Element | null): boolean {
|
|
23
|
+
if (!element) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
if (element instanceof HTMLTextAreaElement) {
|
|
27
|
+
return !element.disabled && !element.readOnly;
|
|
28
|
+
}
|
|
29
|
+
if (element instanceof HTMLInputElement) {
|
|
30
|
+
return !element.disabled && !element.readOnly && !NON_TEXT_INPUT_TYPES.has(element.type);
|
|
31
|
+
}
|
|
32
|
+
return element instanceof HTMLElement && element.isContentEditable;
|
|
33
|
+
}
|
|
34
|
+
|
|
9
35
|
export function useMobileBreakpoint(): boolean {
|
|
10
36
|
const [isMobile, setIsMobile] = useState(() => typeof window !== "undefined" && window.matchMedia(MOBILE_MEDIA_QUERY).matches);
|
|
11
37
|
|
|
@@ -50,6 +76,13 @@ export function useMobileViewportHeight(isMobile: boolean): void {
|
|
|
50
76
|
root.style.removeProperty("--wapp-viewport-height");
|
|
51
77
|
};
|
|
52
78
|
|
|
79
|
+
const shouldOverrideDynamicViewportHeight = () => {
|
|
80
|
+
if (!usesDynamicViewportUnit || !viewport || !isEditableElement(document.activeElement)) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
return viewport.height + 1 < window.innerHeight;
|
|
84
|
+
};
|
|
85
|
+
|
|
53
86
|
const sync = () => {
|
|
54
87
|
frame = 0;
|
|
55
88
|
if (!isMobile) {
|
|
@@ -57,7 +90,8 @@ export function useMobileViewportHeight(isMobile: boolean): void {
|
|
|
57
90
|
return;
|
|
58
91
|
}
|
|
59
92
|
|
|
60
|
-
|
|
93
|
+
const shouldUseVisualViewport = !usesDynamicViewportUnit || shouldOverrideDynamicViewportHeight();
|
|
94
|
+
if (!shouldUseVisualViewport) {
|
|
61
95
|
clearViewportHeight();
|
|
62
96
|
} else {
|
|
63
97
|
const height = Math.round(viewport?.height ?? window.innerHeight);
|
|
@@ -89,7 +123,7 @@ export function useMobileViewportHeight(isMobile: boolean): void {
|
|
|
89
123
|
|
|
90
124
|
const handleViewportTransition = () => {
|
|
91
125
|
scheduleSync();
|
|
92
|
-
if (isMobile
|
|
126
|
+
if (isMobile) {
|
|
93
127
|
scheduleViewportRetry(MOBILE_VIEWPORT_FIRST_SETTLE_DELAY_MS);
|
|
94
128
|
scheduleViewportRetry(MOBILE_VIEWPORT_FINAL_SETTLE_DELAY_MS);
|
|
95
129
|
}
|
package/src/web/root-types.ts
CHANGED
|
@@ -11,12 +11,15 @@ export type SettingsAction = {
|
|
|
11
11
|
onAction: () => void;
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
+
export type SettingsRowContentPlacement = "below" | "inline";
|
|
15
|
+
|
|
14
16
|
export type SettingsRow = {
|
|
15
17
|
id: string;
|
|
16
18
|
title: string;
|
|
17
19
|
description?: string;
|
|
18
20
|
scope?: SettingsScope;
|
|
19
21
|
content?: ReactNode;
|
|
22
|
+
contentPlacement?: SettingsRowContentPlacement;
|
|
20
23
|
actions?: ReactNode | SettingsAction[];
|
|
21
24
|
danger?: boolean;
|
|
22
25
|
};
|
|
@@ -84,25 +84,29 @@ export function SecuritySection({ config, refresh, setError }: SecuritySectionPr
|
|
|
84
84
|
</div>
|
|
85
85
|
</div>
|
|
86
86
|
{config.apiKeys.enabled ? (
|
|
87
|
-
|
|
88
|
-
<div>
|
|
89
|
-
<
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
<div className="wapp-row-actions"><Button type="button" onClick={() => void createKey().catch((err) => setError(String(err)))}>Create API key</Button></div>
|
|
93
|
-
{createdToken ? <code className="wapp-token">{createdToken}</code> : null}
|
|
94
|
-
<ResourceState loading={apiKeysLoading} error={apiKeysLoadError} hasData={apiKeys !== undefined} refresh={refreshApiKeys} />
|
|
95
|
-
{apiKeys?.length ? (
|
|
96
|
-
<div className="wapp-list">
|
|
97
|
-
{apiKeys.map((key) => (
|
|
98
|
-
<div className="wapp-list-row" key={key.id}>
|
|
99
|
-
<span><strong>{key.name}</strong><small>{key.scopes.join(", ")} · {key.createdAt}</small></span>
|
|
100
|
-
<Button type="button" variant="danger" onClick={() => setApiKeyToDelete(key)}>Delete</Button>
|
|
101
|
-
</div>
|
|
102
|
-
))}
|
|
87
|
+
<>
|
|
88
|
+
<div className="wapp-settings-row">
|
|
89
|
+
<div className="wapp-settings-row-main">
|
|
90
|
+
<strong>API keys</strong>
|
|
91
|
+
<p>Create bearer tokens for scripts and agents.</p>
|
|
103
92
|
</div>
|
|
104
|
-
|
|
105
|
-
|
|
93
|
+
<div className="wapp-row-actions"><Button type="button" onClick={() => void createKey().catch((err) => setError(String(err)))}>Create API key</Button></div>
|
|
94
|
+
</div>
|
|
95
|
+
<div className="wapp-settings-row-content">
|
|
96
|
+
{createdToken ? <code className="wapp-token">{createdToken}</code> : null}
|
|
97
|
+
<ResourceState loading={apiKeysLoading} error={apiKeysLoadError} hasData={apiKeys !== undefined} refresh={refreshApiKeys} />
|
|
98
|
+
{apiKeys?.length ? (
|
|
99
|
+
<div className="wapp-list">
|
|
100
|
+
{apiKeys.map((key) => (
|
|
101
|
+
<div className="wapp-list-row" key={key.id}>
|
|
102
|
+
<span><strong>{key.name}</strong><small>{key.scopes.join(", ")} · {key.createdAt}</small></span>
|
|
103
|
+
<Button type="button" variant="danger" onClick={() => setApiKeyToDelete(key)}>Delete</Button>
|
|
104
|
+
</div>
|
|
105
|
+
))}
|
|
106
|
+
</div>
|
|
107
|
+
) : apiKeys !== undefined && !apiKeysLoadError ? <EmptyState title="No API keys" /> : null}
|
|
108
|
+
</div>
|
|
109
|
+
</>
|
|
106
110
|
) : null}
|
|
107
111
|
<ConfirmDialog
|
|
108
112
|
open={Boolean(apiKeyToDelete)}
|
|
@@ -34,13 +34,15 @@ function StructuredSettingsSection({ section }: { section: SettingsSection }) {
|
|
|
34
34
|
if (row.danger) {
|
|
35
35
|
return <DangerZone key={row.id} title={row.title} description={row.description} actions={actions} />;
|
|
36
36
|
}
|
|
37
|
+
const hasInlineContent = Boolean(row.content && row.contentPlacement === "inline");
|
|
37
38
|
return (
|
|
38
|
-
<div className=
|
|
39
|
-
<div>
|
|
39
|
+
<div className={`wapp-settings-row${hasInlineContent ? " inline-content" : ""}`} key={row.id}>
|
|
40
|
+
<div className="wapp-settings-row-main">
|
|
40
41
|
<strong>{row.title}</strong>
|
|
41
42
|
{row.description ? <p>{row.description}</p> : null}
|
|
42
|
-
{row.content ? <div className="wapp-settings-row-content">{row.content}</div> : null}
|
|
43
|
+
{row.content && !hasInlineContent ? <div className="wapp-settings-row-content">{row.content}</div> : null}
|
|
43
44
|
</div>
|
|
45
|
+
{hasInlineContent ? <div className="wapp-settings-row-content inline">{row.content}</div> : null}
|
|
44
46
|
{actions ? <div className="wapp-row-actions">{actions}</div> : null}
|
|
45
47
|
</div>
|
|
46
48
|
);
|
package/src/web/styles.css
CHANGED
|
@@ -3,12 +3,17 @@
|
|
|
3
3
|
--wapp-bg: var(--wapp-shell-bg);
|
|
4
4
|
--wapp-shell-bg: rgb(249 250 251 / 0.95);
|
|
5
5
|
--wapp-surface: #ffffff;
|
|
6
|
+
--wapp-surface-raised: #ffffff;
|
|
6
7
|
--wapp-surface-muted: #f9fafb;
|
|
8
|
+
--wapp-surface-inset: #f9fafb;
|
|
7
9
|
--wapp-border: #d1d5db;
|
|
8
10
|
--wapp-border-soft: #e5e7eb;
|
|
11
|
+
--wapp-border-muted: #d1d5db;
|
|
9
12
|
--wapp-text: #111827;
|
|
10
13
|
--wapp-muted: #4b5563;
|
|
11
14
|
--wapp-muted-2: #64748b;
|
|
15
|
+
--wapp-surface-pill: #e5e7eb;
|
|
16
|
+
--wapp-pill-text: #4b5563;
|
|
12
17
|
--wapp-danger: #991b1b;
|
|
13
18
|
--wapp-danger-bg: #991b1b;
|
|
14
19
|
--wapp-toast-success: #166534;
|
|
@@ -53,12 +58,17 @@
|
|
|
53
58
|
--wapp-bg: var(--wapp-shell-bg);
|
|
54
59
|
--wapp-shell-bg: rgb(23 23 23 / 0.95);
|
|
55
60
|
--wapp-surface: #262626;
|
|
61
|
+
--wapp-surface-raised: #171717;
|
|
56
62
|
--wapp-surface-muted: #171717;
|
|
63
|
+
--wapp-surface-inset: rgb(10 10 10 / 0.5);
|
|
57
64
|
--wapp-border: #374151;
|
|
58
65
|
--wapp-border-soft: #27272a;
|
|
66
|
+
--wapp-border-muted: #1f2937;
|
|
59
67
|
--wapp-text: #f3f4f6;
|
|
60
68
|
--wapp-muted: #a1a1aa;
|
|
61
69
|
--wapp-muted-2: #71717a;
|
|
70
|
+
--wapp-surface-pill: #262626;
|
|
71
|
+
--wapp-pill-text: #d1d5db;
|
|
62
72
|
--wapp-danger: #fca5a5;
|
|
63
73
|
--wapp-danger-bg: #7f1d1d;
|
|
64
74
|
--wapp-toast-success: #bbf7d0;
|
|
@@ -693,7 +703,7 @@ textarea::placeholder {
|
|
|
693
703
|
overflow-wrap: anywhere;
|
|
694
704
|
}
|
|
695
705
|
|
|
696
|
-
.wapp-panel {
|
|
706
|
+
:where(.wapp-panel) {
|
|
697
707
|
min-width: 0;
|
|
698
708
|
max-width: 100%;
|
|
699
709
|
box-sizing: border-box;
|
|
@@ -703,10 +713,34 @@ textarea::placeholder {
|
|
|
703
713
|
padding: 1.25rem;
|
|
704
714
|
}
|
|
705
715
|
|
|
716
|
+
.wapp-panel-muted {
|
|
717
|
+
border-color: var(--wapp-border-muted);
|
|
718
|
+
border-radius: 1rem;
|
|
719
|
+
background: var(--wapp-surface-inset);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
.wapp-panel-plain {
|
|
723
|
+
border-color: transparent;
|
|
724
|
+
background: transparent;
|
|
725
|
+
padding: 0;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
.wapp-panel-padding-compact {
|
|
729
|
+
padding: 1rem;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
.wapp-panel-padding-none {
|
|
733
|
+
padding: 0;
|
|
734
|
+
}
|
|
735
|
+
|
|
706
736
|
.wapp-panel + .wapp-panel {
|
|
707
737
|
margin-top: 1.5rem;
|
|
708
738
|
}
|
|
709
739
|
|
|
740
|
+
.grid > .wapp-panel {
|
|
741
|
+
margin-top: 0;
|
|
742
|
+
}
|
|
743
|
+
|
|
710
744
|
.wapp-panel-header {
|
|
711
745
|
display: flex;
|
|
712
746
|
align-items: flex-start;
|
|
@@ -727,6 +761,7 @@ textarea::placeholder {
|
|
|
727
761
|
margin: 0;
|
|
728
762
|
font-size: 1.125rem;
|
|
729
763
|
font-weight: 600;
|
|
764
|
+
line-height: 1.75rem;
|
|
730
765
|
}
|
|
731
766
|
|
|
732
767
|
.wapp-panel-header p,
|
|
@@ -844,6 +879,12 @@ textarea::placeholder {
|
|
|
844
879
|
border-radius: 0.875rem;
|
|
845
880
|
}
|
|
846
881
|
|
|
882
|
+
.wapp-data-list-cards {
|
|
883
|
+
overflow: visible;
|
|
884
|
+
border: 0;
|
|
885
|
+
border-radius: 0;
|
|
886
|
+
}
|
|
887
|
+
|
|
847
888
|
.wapp-data-list-row {
|
|
848
889
|
box-sizing: border-box;
|
|
849
890
|
width: 100%;
|
|
@@ -857,6 +898,68 @@ textarea::placeholder {
|
|
|
857
898
|
text-align: left;
|
|
858
899
|
}
|
|
859
900
|
|
|
901
|
+
.wapp-data-list-row-card,
|
|
902
|
+
.wapp-data-list-cards .wapp-data-list-row {
|
|
903
|
+
align-items: flex-start;
|
|
904
|
+
border: 1px solid var(--wapp-border-muted);
|
|
905
|
+
border-radius: 1rem;
|
|
906
|
+
background: var(--wapp-surface-raised);
|
|
907
|
+
padding: 0.75rem 1rem;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
.wapp-data-list-row-card + .wapp-data-list-row-card,
|
|
911
|
+
.wapp-data-list-cards .wapp-data-list-row + .wapp-data-list-row {
|
|
912
|
+
margin-top: 0.5rem;
|
|
913
|
+
border-top: 1px solid var(--wapp-border-muted);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
.wapp-data-list-row-card .wapp-data-list-row-main strong,
|
|
917
|
+
.wapp-data-list-cards .wapp-data-list-row .wapp-data-list-row-main strong {
|
|
918
|
+
overflow: visible;
|
|
919
|
+
overflow-wrap: anywhere;
|
|
920
|
+
font-weight: 500;
|
|
921
|
+
line-height: 1.25rem;
|
|
922
|
+
white-space: normal;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
.wapp-data-list-row-card .wapp-data-list-row-main small,
|
|
926
|
+
.wapp-data-list-cards .wapp-data-list-row .wapp-data-list-row-main small {
|
|
927
|
+
font-size: 0.75rem;
|
|
928
|
+
line-height: 1rem;
|
|
929
|
+
overflow-wrap: anywhere;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
.wapp-data-list-row-card .wapp-data-list-row-meta,
|
|
933
|
+
.wapp-data-list-cards .wapp-data-list-row .wapp-data-list-row-meta {
|
|
934
|
+
border-radius: 999px;
|
|
935
|
+
background: var(--wapp-surface-pill);
|
|
936
|
+
color: var(--wapp-pill-text);
|
|
937
|
+
font-size: 0.75rem;
|
|
938
|
+
font-weight: 600;
|
|
939
|
+
padding: 0.125rem 0.5rem;
|
|
940
|
+
text-align: right;
|
|
941
|
+
white-space: nowrap;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
.wapp-data-list-row-meta-below {
|
|
945
|
+
display: block;
|
|
946
|
+
font-size: 0.75rem;
|
|
947
|
+
line-height: 1rem;
|
|
948
|
+
margin-top: 0.5rem;
|
|
949
|
+
overflow-wrap: anywhere;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
.wapp-data-list-row-card.interactive:hover,
|
|
953
|
+
.wapp-data-list-cards .wapp-data-list-row.interactive:hover {
|
|
954
|
+
border-color: var(--wapp-border);
|
|
955
|
+
background: var(--wapp-surface-pill);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
.wapp-data-list-row.disabled {
|
|
959
|
+
cursor: default;
|
|
960
|
+
opacity: 0.65;
|
|
961
|
+
}
|
|
962
|
+
|
|
860
963
|
.wapp-data-list-row + .wapp-data-list-row {
|
|
861
964
|
border-top: 1px solid var(--wapp-border-soft);
|
|
862
965
|
}
|
|
@@ -983,6 +1086,30 @@ textarea::placeholder {
|
|
|
983
1086
|
transition: background 120ms ease, border-color 120ms ease, color 120ms ease, transform 120ms ease;
|
|
984
1087
|
}
|
|
985
1088
|
|
|
1089
|
+
.wapp-button-xs {
|
|
1090
|
+
min-height: 2rem;
|
|
1091
|
+
padding: 0.25rem 0.375rem;
|
|
1092
|
+
font-size: 0.75rem;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
.wapp-button-sm {
|
|
1096
|
+
min-height: 2.25rem;
|
|
1097
|
+
padding: 0.25rem 0.5rem;
|
|
1098
|
+
font-size: 0.875rem;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
.wapp-button-md {
|
|
1102
|
+
min-height: 2.75rem;
|
|
1103
|
+
padding: 0.5rem 1rem;
|
|
1104
|
+
font-size: 1rem;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
.wapp-button-lg {
|
|
1108
|
+
min-height: 3rem;
|
|
1109
|
+
padding: 0.75rem 1.5rem;
|
|
1110
|
+
font-size: 1.125rem;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
986
1113
|
.wapp-button-spinner {
|
|
987
1114
|
width: 1rem;
|
|
988
1115
|
height: 1rem;
|
|
@@ -1152,6 +1279,16 @@ textarea::placeholder {
|
|
|
1152
1279
|
padding: 0.125rem 0.5rem;
|
|
1153
1280
|
}
|
|
1154
1281
|
|
|
1282
|
+
.wapp-badge-md {
|
|
1283
|
+
font-size: 0.75rem;
|
|
1284
|
+
padding: 0.25rem 0.625rem;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
.wapp-status-badge {
|
|
1288
|
+
text-transform: uppercase;
|
|
1289
|
+
letter-spacing: 0.05em;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1155
1292
|
.wapp-sidebar-badge {
|
|
1156
1293
|
flex: 0 0 auto;
|
|
1157
1294
|
width: 0.625rem;
|
|
@@ -1359,6 +1496,16 @@ textarea::placeholder {
|
|
|
1359
1496
|
gap: 1rem;
|
|
1360
1497
|
}
|
|
1361
1498
|
|
|
1499
|
+
.wapp-settings-row-main {
|
|
1500
|
+
min-width: 0;
|
|
1501
|
+
flex: 1 1 auto;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
.wapp-settings-row.inline-content {
|
|
1505
|
+
flex-wrap: wrap;
|
|
1506
|
+
align-items: center;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1362
1509
|
.wapp-settings-row strong {
|
|
1363
1510
|
font-size: 0.875rem;
|
|
1364
1511
|
font-weight: 600;
|
|
@@ -1383,6 +1530,12 @@ textarea::placeholder {
|
|
|
1383
1530
|
margin-top: 0.75rem;
|
|
1384
1531
|
}
|
|
1385
1532
|
|
|
1533
|
+
.wapp-settings-row-content.inline {
|
|
1534
|
+
flex: 0 0 auto;
|
|
1535
|
+
align-self: center;
|
|
1536
|
+
margin-top: 0;
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1386
1539
|
.wapp-row-actions {
|
|
1387
1540
|
display: flex;
|
|
1388
1541
|
flex-wrap: wrap;
|
|
@@ -2084,6 +2237,15 @@ textarea::placeholder {
|
|
|
2084
2237
|
display: block;
|
|
2085
2238
|
}
|
|
2086
2239
|
|
|
2240
|
+
:root[data-wapp-mobile] .wapp-settings-row.inline-content {
|
|
2241
|
+
display: flex;
|
|
2242
|
+
align-items: center;
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
:root[data-wapp-mobile] .wapp-settings-row.inline-content .wapp-row-actions {
|
|
2246
|
+
flex: 1 0 100%;
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2087
2249
|
:root[data-wapp-mobile] .wapp-row-actions {
|
|
2088
2250
|
justify-content: flex-start;
|
|
2089
2251
|
margin-top: 0.75rem;
|