@pablozaiden/webapp 0.6.8 → 0.7.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 +3 -1
- package/package.json +1 -1
- package/src/web/WebAppRoot.tsx +8 -1
- package/src/web/components/index.tsx +77 -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 +158 -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
|
|
|
@@ -56,6 +56,8 @@ Use these first:
|
|
|
56
56
|
| `EmptyState` | Empty or missing content |
|
|
57
57
|
| `ConfirmDialog` | Destructive confirmation |
|
|
58
58
|
|
|
59
|
+
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.
|
|
60
|
+
|
|
59
61
|
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
62
|
|
|
61
63
|
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,17 @@ 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 function Badge({
|
|
36
|
+
variant = "default",
|
|
37
|
+
size = "sm",
|
|
38
|
+
className = "",
|
|
39
|
+
children,
|
|
40
|
+
...props
|
|
41
|
+
}: HTMLAttributes<HTMLSpanElement> & { variant?: BadgeVariant; size?: BadgeSize; children: ReactNode }) {
|
|
42
|
+
const sizeClass = size === "md" ? "wapp-badge-md" : "";
|
|
43
|
+
return <span {...props} className={["wapp-badge", `wapp-badge-${variant}`, sizeClass, className].filter(Boolean).join(" ")}>{children}</span>;
|
|
31
44
|
}
|
|
32
45
|
|
|
33
46
|
export type PageLayout = "padded" | "full";
|
|
@@ -41,9 +54,31 @@ export function Page({
|
|
|
41
54
|
return <div {...props} className={`wapp-page ${layout === "full" ? "wapp-page-full" : ""} ${className}`.trim()}>{children}</div>;
|
|
42
55
|
}
|
|
43
56
|
|
|
44
|
-
export
|
|
57
|
+
export type PanelVariant = "surface" | "muted" | "plain";
|
|
58
|
+
export type PanelPadding = "default" | "compact" | "none";
|
|
59
|
+
|
|
60
|
+
export interface PanelProps extends HTMLAttributes<HTMLElement> {
|
|
61
|
+
title?: string;
|
|
62
|
+
description?: string;
|
|
63
|
+
actions?: ReactNode;
|
|
64
|
+
variant?: PanelVariant;
|
|
65
|
+
padding?: PanelPadding;
|
|
66
|
+
children?: ReactNode;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function Panel({
|
|
70
|
+
title,
|
|
71
|
+
description,
|
|
72
|
+
actions,
|
|
73
|
+
children,
|
|
74
|
+
variant = "muted",
|
|
75
|
+
padding = "default",
|
|
76
|
+
className = "",
|
|
77
|
+
...props
|
|
78
|
+
}: PanelProps) {
|
|
79
|
+
const paddingClass = padding === "default" ? "" : `wapp-panel-padding-${padding}`;
|
|
45
80
|
return (
|
|
46
|
-
<section className={`wapp-panel ${className}
|
|
81
|
+
<section {...props} className={`wapp-panel wapp-panel-${variant} ${paddingClass} ${className}`.trim()}>
|
|
47
82
|
{title || description || actions ? (
|
|
48
83
|
<div className="wapp-panel-header">
|
|
49
84
|
<div>
|
|
@@ -111,40 +146,68 @@ export function EntityHeader({
|
|
|
111
146
|
);
|
|
112
147
|
}
|
|
113
148
|
|
|
114
|
-
export
|
|
115
|
-
|
|
149
|
+
export type DataListVariant = "divided" | "cards";
|
|
150
|
+
|
|
151
|
+
export function DataList({
|
|
152
|
+
children,
|
|
153
|
+
empty,
|
|
154
|
+
variant = "cards",
|
|
155
|
+
}: {
|
|
156
|
+
children?: ReactNode;
|
|
157
|
+
empty?: ReactNode;
|
|
158
|
+
variant?: DataListVariant;
|
|
159
|
+
}) {
|
|
160
|
+
return <div className={`wapp-data-list wapp-data-list-${variant}`}>{children ?? empty ?? null}</div>;
|
|
116
161
|
}
|
|
117
162
|
|
|
118
163
|
export function DataListRow({
|
|
119
164
|
title,
|
|
120
165
|
description,
|
|
166
|
+
descriptionClassName = "",
|
|
121
167
|
meta,
|
|
168
|
+
metaPlacement = "side",
|
|
122
169
|
badge,
|
|
123
170
|
actions,
|
|
124
171
|
onClick,
|
|
172
|
+
disabled = false,
|
|
173
|
+
variant = "card",
|
|
174
|
+
className = "",
|
|
125
175
|
}: {
|
|
126
176
|
title: ReactNode;
|
|
127
177
|
description?: ReactNode;
|
|
178
|
+
descriptionClassName?: string;
|
|
128
179
|
meta?: ReactNode;
|
|
180
|
+
metaPlacement?: "side" | "below";
|
|
129
181
|
badge?: ReactNode;
|
|
130
182
|
actions?: ReactNode;
|
|
131
183
|
onClick?: () => void;
|
|
184
|
+
disabled?: boolean;
|
|
185
|
+
variant?: "default" | "card";
|
|
186
|
+
className?: string;
|
|
132
187
|
}) {
|
|
188
|
+
const rowClassName = [
|
|
189
|
+
"wapp-data-list-row",
|
|
190
|
+
variant === "card" ? "wapp-data-list-row-card" : "",
|
|
191
|
+
onClick && !disabled ? "interactive" : "",
|
|
192
|
+
disabled ? "disabled" : "",
|
|
193
|
+
className,
|
|
194
|
+
].filter(Boolean).join(" ");
|
|
133
195
|
const content = (
|
|
134
196
|
<>
|
|
135
197
|
<span className="wapp-data-list-row-main">
|
|
136
198
|
<strong>{title}</strong>
|
|
137
|
-
{description ? <small>{description}</small> : null}
|
|
199
|
+
{description ? <small className={descriptionClassName}>{description}</small> : null}
|
|
200
|
+
{meta && metaPlacement === "below" ? <small className="wapp-data-list-row-meta-below">{meta}</small> : null}
|
|
138
201
|
</span>
|
|
139
|
-
{meta ? <span className="wapp-data-list-row-meta">{meta}</span> : null}
|
|
202
|
+
{meta && metaPlacement === "side" ? <span className="wapp-data-list-row-meta">{meta}</span> : null}
|
|
140
203
|
{badge ? <span className="wapp-data-list-row-badge">{badge}</span> : null}
|
|
141
204
|
{actions ? <span className="wapp-data-list-row-actions">{actions}</span> : null}
|
|
142
205
|
</>
|
|
143
206
|
);
|
|
144
|
-
return onClick ? (
|
|
145
|
-
<button type="button" className=
|
|
207
|
+
return onClick && !disabled ? (
|
|
208
|
+
<button type="button" className={rowClassName} onClick={onClick}>{content}</button>
|
|
146
209
|
) : (
|
|
147
|
-
<div className=
|
|
210
|
+
<div className={rowClassName}>{content}</div>
|
|
148
211
|
);
|
|
149
212
|
}
|
|
150
213
|
|
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,11 @@ 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
|
+
|
|
1155
1287
|
.wapp-sidebar-badge {
|
|
1156
1288
|
flex: 0 0 auto;
|
|
1157
1289
|
width: 0.625rem;
|
|
@@ -1359,6 +1491,16 @@ textarea::placeholder {
|
|
|
1359
1491
|
gap: 1rem;
|
|
1360
1492
|
}
|
|
1361
1493
|
|
|
1494
|
+
.wapp-settings-row-main {
|
|
1495
|
+
min-width: 0;
|
|
1496
|
+
flex: 1 1 auto;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
.wapp-settings-row.inline-content {
|
|
1500
|
+
flex-wrap: wrap;
|
|
1501
|
+
align-items: center;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1362
1504
|
.wapp-settings-row strong {
|
|
1363
1505
|
font-size: 0.875rem;
|
|
1364
1506
|
font-weight: 600;
|
|
@@ -1383,6 +1525,12 @@ textarea::placeholder {
|
|
|
1383
1525
|
margin-top: 0.75rem;
|
|
1384
1526
|
}
|
|
1385
1527
|
|
|
1528
|
+
.wapp-settings-row-content.inline {
|
|
1529
|
+
flex: 0 0 auto;
|
|
1530
|
+
align-self: center;
|
|
1531
|
+
margin-top: 0;
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1386
1534
|
.wapp-row-actions {
|
|
1387
1535
|
display: flex;
|
|
1388
1536
|
flex-wrap: wrap;
|
|
@@ -2084,6 +2232,15 @@ textarea::placeholder {
|
|
|
2084
2232
|
display: block;
|
|
2085
2233
|
}
|
|
2086
2234
|
|
|
2235
|
+
:root[data-wapp-mobile] .wapp-settings-row.inline-content {
|
|
2236
|
+
display: flex;
|
|
2237
|
+
align-items: center;
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
:root[data-wapp-mobile] .wapp-settings-row.inline-content .wapp-row-actions {
|
|
2241
|
+
flex: 1 0 100%;
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2087
2244
|
:root[data-wapp-mobile] .wapp-row-actions {
|
|
2088
2245
|
justify-content: flex-start;
|
|
2089
2246
|
margin-top: 0.75rem;
|