ui: improve desktop/mobile usability and long i18n label layout
- Global CSS: wrap long translations, safer forms/modals/tables on mobile - PageHeader stacks actions on narrow screens - Users mobile cards show traffic, expiry, remark/tags without losing info - Layout content width + drawer width polish ui: improve desktop/mobile usability and long i18n label layout - Global CSS: wrap long translations, safer forms/modals/tables on mobile - PageHeader stacks actions on narrow screens - Users mobile cards show traffic, expiry, remark/tags without losing info - Layout content width + drawer width polish ui: improve desktop/mobile usability and long i18n label layout - Global CSS: wrap long translations, safer forms/modals/tables on mobile - PageHeader stacks actions on narrow screens - Users mobile cards show traffic, expiry, remark/tags without losing info - Layout content width + drawer width polish ui: improve desktop/mobile usability and long i18n label layout - Global CSS: wrap long translations, safer forms/modals/tables on mobile - PageHeader stacks actions on narrow screens - Users mobile cards show traffic, expiry, remark/tags without losing info - Layout content width + drawer width polish ui: improve desktop/mobile usability and long i18n label layout - Global CSS: wrap long translations, safer forms/modals/tables on mobile - PageHeader stacks actions on narrow screens - Users mobile cards show traffic, expiry, remark/tags without losing info - Layout content width + drawer width polish ui: improve desktop/mobile usability and long i18n label layout - Global CSS: wrap long translations, safer forms/modals/tables on mobile - PageHeader stacks actions on narrow screens - Users mobile cards show traffic, expiry, remark/tags without losing info - Layout content width + drawer width polish ui: improve desktop/mobile usability and long i18n label layout - Global CSS: wrap long translations, safer forms/modals/tables on mobile - PageHeader stacks actions on narrow screens - Users mobile cards show traffic, expiry, remark/tags without losing info - Layout content width + drawer width polish ui: improve desktop/mobile usability and long i18n label layout - Global CSS: wrap long translations, safer forms/modals/tables on mobile - PageHeader stacks actions on narrow screens - Users mobile cards show traffic, expiry, remark/tags without losing info - Layout content width + drawer width polish ui: improve desktop/mobile usability and long i18n label layout - Global CSS: wrap long translations, safer forms/modals/tables on mobile - PageHeader stacks actions on narrow screens - Users mobile cards show traffic, expiry, remark/tags without losing info - Layout content width + drawer width polish ui: improve desktop/mobile usability and long i18n label layout - Global CSS: wrap long translations, safer forms/modals/tables on mobile - PageHeader stacks actions on narrow screens - Users mobile cards show traffic, expiry, remark/tags without losing info - Layout content width + drawer width polish ui: improve desktop/mobile usability and long i18n label layout - Global CSS: wrap long translations, safer forms/modals/tables on mobile - PageHeader stacks actions on narrow screens - Users mobile cards show traffic, expiry, remark/tags without losing info - Layout content width + drawer width polish ui: improve desktop/mobile usability and long i18n label layout - Global CSS: wrap long translations, safer forms/modals/tables on mobile - PageHeader stacks actions on narrow screens - Users mobile cards show traffic, expiry, remark/tags without losing info - Layout content width + drawer width polish
This commit is contained in:
1 parent
4d6cb94145
commit
e332cfdac3
7 files changed
+232
-72
No files matched your search
@@ -14,7 +14,6 @@ const AppLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
token: { colorBgContainer, borderRadiusLG },
|
||||
} = theme.useToken();
|
||||
|
||||
// Close drawer when switching to desktop
|
||||
useEffect(() => {
|
||||
if (!isMobile) setDrawerOpen(false);
|
||||
}, [isMobile]);
|
||||
@@ -39,6 +38,8 @@ const AppLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
overflow: 'auto',
|
||||
minWidth: 0,
|
||||
maxWidth: isMobile ? undefined : 1400,
|
||||
width: isMobile ? undefined : 'calc(100% - 48px)',
|
||||
alignSelf: isMobile ? undefined : 'center',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
@@ -50,7 +51,7 @@ const AppLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
placement="left"
|
||||
open={isMobile && drawerOpen}
|
||||
onClose={() => setDrawerOpen(false)}
|
||||
width={280}
|
||||
width={Math.min(300, typeof window !== 'undefined' ? window.innerWidth - 24 : 280)}
|
||||
styles={{ body: { padding: 0 } }}
|
||||
title={null}
|
||||
closable={false}
|
||||
|
||||
@@ -85,7 +85,7 @@ const HeaderBar: React.FC<Props> = ({ collapsed, setCollapsed, onOpenMobileNav }
|
||||
{!isMobile && (LOCALE_OPTIONS.find((o) => o.key === locale)?.label || locale)}
|
||||
</Button>
|
||||
</Dropdown>
|
||||
<Tag icon={<UserOutlined />} style={{ marginInlineEnd: 0 }}>
|
||||
<Tag icon={<UserOutlined />} title={displayName} style={{ marginInlineEnd: 0 }}>
|
||||
{isMobile ? shortName : displayName}
|
||||
</Tag>
|
||||
</Space>
|
||||
|
||||
@@ -2,56 +2,52 @@ import React from 'react';
|
||||
import { Typography, Space } from 'antd';
|
||||
import useIsMobile from '../hooks/useIsMobile';
|
||||
|
||||
const { Title } = Typography;
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
/** Consistent page title + optional subtitle/actions for mobile & desktop. */
|
||||
const PageHeader: React.FC<{
|
||||
export interface PageHeaderProps {
|
||||
title: React.ReactNode;
|
||||
subtitle?: React.ReactNode;
|
||||
extra?: React.ReactNode;
|
||||
}> = ({ title, subtitle, extra }) => {
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared page title block. Stacks actions under the title on narrow screens
|
||||
* so long translations (DE/RU/AR/…) remain readable and tappable.
|
||||
*/
|
||||
const PageHeader: React.FC<PageHeaderProps> = ({ title, subtitle, extra, style }) => {
|
||||
const isMobile = useIsMobile();
|
||||
const sub =
|
||||
subtitle === undefined || subtitle === null || subtitle === false || subtitle === ''
|
||||
? null
|
||||
: subtitle;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="page-header"
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: isMobile ? 'column' : 'row',
|
||||
alignItems: isMobile ? 'stretch' : 'flex-start',
|
||||
justifyContent: 'space-between',
|
||||
gap: isMobile ? 8 : 16,
|
||||
marginBottom: isMobile ? 12 : 16,
|
||||
}}
|
||||
>
|
||||
<div style={{ minWidth: 0, flex: 1 }}>
|
||||
<Title level={isMobile ? 4 : 3} style={{ margin: 0 }}>
|
||||
{title}
|
||||
</Title>
|
||||
{sub != null ? (
|
||||
<p
|
||||
className="page-header-subtitle"
|
||||
style={{
|
||||
margin: '6px 0 0',
|
||||
fontSize: isMobile ? 13 : 14,
|
||||
lineHeight: 1.45,
|
||||
opacity: 0.7,
|
||||
color: 'var(--ant-color-text-secondary, inherit)',
|
||||
}}
|
||||
>
|
||||
{sub}
|
||||
</p>
|
||||
<div className="page-header" style={{ marginBottom: isMobile ? 12 : 20, ...style }}>
|
||||
<div
|
||||
className="page-header-title-row"
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: isMobile ? 'stretch' : 'flex-start',
|
||||
justifyContent: 'space-between',
|
||||
gap: 12,
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
<div style={{ minWidth: 0, flex: '1 1 200px' }}>
|
||||
<Title level={isMobile ? 4 : 3} style={{ margin: 0, lineHeight: 1.3 }}>
|
||||
{title}
|
||||
</Title>
|
||||
{subtitle ? (
|
||||
<Text type="secondary" className="page-header-subtitle" style={{ display: 'block', marginTop: 4 }}>
|
||||
{subtitle}
|
||||
</Text>
|
||||
) : null}
|
||||
</div>
|
||||
{extra ? (
|
||||
<div className="page-header-actions" style={{ flex: isMobile ? '1 1 100%' : '0 0 auto' }}>
|
||||
<Space wrap size="small" style={{ width: isMobile ? '100%' : undefined }}>
|
||||
{extra}
|
||||
</Space>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{extra ? (
|
||||
<Space wrap style={{ width: isMobile ? '100%' : undefined }}>
|
||||
{extra}
|
||||
</Space>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -46,7 +46,7 @@ const Dashboard: React.FC = () => {
|
||||
return (
|
||||
<div>
|
||||
<PageHeader title={t('dashboard.title')} subtitle={t('dashboard.subtitle')} />
|
||||
<Row gutter={[16, 16]}>
|
||||
<Row gutter={isMobile ? [12, 12] : [16, 16]}>
|
||||
<Col xs={24} md={12} lg={8}>
|
||||
<Card title={t('dashboard.users') || 'Users'}>
|
||||
<Statistic title={t('dashboard.onlineUsers') || 'Online'} value={users.online ?? data?.onlineUsers ?? 0} />
|
||||
@@ -73,7 +73,7 @@ const Dashboard: React.FC = () => {
|
||||
</Col>
|
||||
<Col xs={24} md={12} lg={8}>
|
||||
<Card title={t('dashboard.listeners')}>
|
||||
<Row gutter={16}>
|
||||
<Row gutter={isMobile ? [12, 12] : 16}>
|
||||
<Col span={8}><Statistic title={t('dashboard.total')} value={data?.listeners?.total || 0} /></Col>
|
||||
<Col span={8}><Statistic title={t('dashboard.enabled')} value={data?.listeners?.enabled || 0} valueStyle={{ color: '#3f8600' }} /></Col>
|
||||
<Col span={8}><Statistic title={t('dashboard.disabled')} value={data?.listeners?.disabled || 0} valueStyle={{ color: '#cf1322' }} /></Col>
|
||||
|
||||
@@ -233,7 +233,7 @@ const columns = [
|
||||
</Spin>
|
||||
) : (
|
||||
<Table rowSelection={{ selectedRowKeys, onChange: setSelectedRowKeys }} dataSource={filteredListeners} columns={columns} rowKey="id" loading={loading} scroll={{ x: 880 }} size="middle" />
|
||||
)}</Card> }, { key: 'templates', label: t('listeners.templates'), children: <Card title={t('listeners.templates')} extra={<Button icon={<ReloadOutlined />} onClick={loadTemplates}>{t('common.refresh')}</Button>}><Table dataSource={templates} columns={templateColumns} rowKey="id" loading={templateLoading} pagination={{ pageSize: 10 }} /></Card> }]} />
|
||||
)}</Card> }, { key: 'templates', label: t('listeners.templates'), children: <Card title={t('listeners.templates')} extra={<Button icon={<ReloadOutlined />} onClick={loadTemplates}>{t('common.refresh')}</Button>}><Table dataSource={templates} columns={templateColumns} rowKey="id" loading={templateLoading} pagination={{ pageSize: 10 }} size={isMobile ? "small" : "middle"} /></Card> }]} />
|
||||
<ListenerRuntimeDrawer listener={runtimeListener ? data.find(l => l.id === runtimeListener.id) || runtimeListener : null} status={runtimeListener ? details[runtimeListener.id] || statuses[runtimeListener.id] : undefined} checking={!!runtimeListener && testing.includes(runtimeListener.id)} onClose={() => setRuntimeListener(null)} onCheck={() => { if (runtimeListener) void test(runtimeListener.id).catch(() => {}); }} />
|
||||
<Modal confirmLoading={submitting} closable={!submitting} maskClosable={!submitting} keyboard={!submitting} cancelButtonProps={{ disabled: submitting }} okText={submitting ? runtimeText.saving : t('common.save')} open={modalOpen} title={editing ? t('listeners.edit') : t('listeners.create')} onCancel={() => { setModalOpen(false); setEditing(null); form.resetFields(); }} onOk={() => form.validateFields().then((vals) => onSubmit(vals))} width={isMobile ? '100%' : 720} style={isMobile ? { top: 8, maxWidth: '100vw', margin: 0, padding: 0 } : undefined} className={isMobile ? 'mobile-full-modal' : undefined} destroyOnClose styles={{ body: { maxHeight: '70vh', overflowY: 'auto' } }}>
|
||||
{submitError && <Alert type="error" showIcon title={runtimeText.failed} description={<><div>{submitError}</div><div>{runtimeText.failedHint}</div></>} style={{ marginBottom: 16 }} />}
|
||||
@@ -256,7 +256,7 @@ const columns = [
|
||||
<Modal open={cloneModal} title={t('listeners.clone')} onCancel={() => setCloneModal(false)} onOk={() => cloneForm.submit()}><Form form={cloneForm} layout="vertical" onFinish={doClone}><Form.Item name="name" label={t('listeners.name')} rules={[{ required: true }]}><Input /></Form.Item><Form.Item name="port" label={t('listeners.newPort')} rules={[{ required: true }]}><Input placeholder="443" /></Form.Item></Form></Modal>
|
||||
<Modal open={templateModal} title={t('listeners.saveTemplate')} onCancel={() => setTemplateModal(false)} onOk={() => templateForm.submit()}><Form form={templateForm} layout="vertical" onFinish={saveTemplate}><Form.Item name="name" label={t('listeners.templateName')} rules={[{ required: true }]}><Input /></Form.Item><Descriptions column={1} size="small"><Descriptions.Item label={t('listeners.protocol')}>{templateSource?.protocol}</Descriptions.Item></Descriptions></Form></Modal>
|
||||
<Modal open={instantiateModal} title={t('listeners.instantiate')} onCancel={() => setInstantiateModal(false)} onOk={() => instantiateForm.submit()}><Form form={instantiateForm} layout="vertical" onFinish={doInstantiate}><Form.Item name="name" label={t('listeners.name')} rules={[{ required: true }]}><Input /></Form.Item><Form.Item name="port" label={t('listeners.newPort')} rules={[{ required: true }]}><Input placeholder="443" /></Form.Item></Form></Modal>
|
||||
<Modal open={versionsModal} title={`${t('listeners.versions')} — ${versionListener?.name || ''}`} onCancel={() => setVersionsModal(false)} footer={null} width={800}><Table dataSource={versions} rowKey="id" pagination={false} columns={[{ title: t('listeners.version'), dataIndex: 'version', width: 100 }, { title: t('listeners.reason'), dataIndex: 'reason', render: (v: string) => v || '-' }, { title: t('listeners.createdAt'), dataIndex: 'created_at', render: (v: string) => new Date(v).toLocaleString() }, { title: t('common.actions'), render: (_: any, v: ListenerVersion) => <Space><Button size="small" icon={<DiffOutlined />} onClick={() => showDiff(v.version)}>{t('listeners.diff')}</Button><Popconfirm title={t('listeners.rollbackConfirm')} onConfirm={() => doRollback(v.version)}><Button size="small" type="primary">{t('listeners.rollback')}</Button></Popconfirm></Space> }]} /></Modal>
|
||||
<Modal open={versionsModal} title={`${t('listeners.versions')} — ${versionListener?.name || ''}`} onCancel={() => setVersionsModal(false)} footer={null} width={800}><Table dataSource={versions} rowKey="id" pagination={false} columns={[{ title: t('listeners.version'), dataIndex: 'version', width: 100 }, { title: t('listeners.reason'), dataIndex: 'reason', render: (v: string) = size={isMobile ? "small" : "middle"}> v || '-' }, { title: t('listeners.createdAt'), dataIndex: 'created_at', render: (v: string) => new Date(v).toLocaleString() }, { title: t('common.actions'), render: (_: any, v: ListenerVersion) => <Space><Button size="small" icon={<DiffOutlined />} onClick={() => showDiff(v.version)}>{t('listeners.diff')}</Button><Popconfirm title={t('listeners.rollbackConfirm')} onConfirm={() => doRollback(v.version)}><Button size="small" type="primary">{t('listeners.rollback')}</Button></Popconfirm></Space> }]} /></Modal>
|
||||
<Modal open={diffModal} title={t('listeners.diff')} onCancel={() => setDiffModal(false)} footer={null} width={900}><pre style={{ maxHeight: '65vh', overflow: 'auto', whiteSpace: 'pre-wrap', wordBreak: 'break-word', margin: 0 }}>{diffText || t('common.empty')}</pre></Modal>
|
||||
<Modal open={uriModal} title={t('listeners.urisTitle')} onCancel={() => setUriModal(false)} footer={null} width={isMobile ? '100%' : 560} style={isMobile ? { top: 8 } : undefined} className={isMobile ? 'mobile-full-modal' : undefined}>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
|
||||
@@ -572,10 +572,30 @@ const Users: React.FC = () => {
|
||||
<Tag color={record.enabled ? 'processing' : 'default'}>
|
||||
{record.enabled ? t('common.enabled') : t('common.disabled')}
|
||||
</Tag>
|
||||
<span className="mobile-entity-sub">
|
||||
{formatBytes(used)}
|
||||
{limit > 0 ? ` / ${formatBytes(limit)}` : ` / ${t('users.unlimited')}`}
|
||||
</span>
|
||||
{record.group ? <Tag>{record.group}</Tag> : null}
|
||||
</div>
|
||||
<div className="mobile-entity-rows">
|
||||
<div className="mobile-entity-row">
|
||||
<span className="label">{t('users.traffic')}</span>
|
||||
<span className="value">
|
||||
{formatBytes(used)}
|
||||
{limit > 0 ? ` / ${formatBytes(limit)}` : ` / ${t('users.unlimited')}`}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mobile-entity-row">
|
||||
<span className="label">{t('users.expire')}</span>
|
||||
<span className="value">
|
||||
{!record.expire_time || String(record.expire_time).startsWith('0001')
|
||||
? t('users.neverExpire')
|
||||
: dayjs(record.expire_time).format('YYYY-MM-DD')}
|
||||
</span>
|
||||
</div>
|
||||
{(record.tags || record.remark) && (
|
||||
<div className="mobile-entity-row">
|
||||
<span className="label">{t('users.remark') || 'Remark'}</span>
|
||||
<span className="value">{[record.remark, record.tags].filter(Boolean).join(' · ')}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Dropdown
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* 3m-ui global layout polish — mobile + desktop */
|
||||
/* 3m-ui global layout polish — mobile + desktop + long i18n labels */
|
||||
|
||||
html, body, #root {
|
||||
max-width: 100vw;
|
||||
@@ -9,30 +9,80 @@ body {
|
||||
padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
|
||||
}
|
||||
|
||||
/* Prevent long translated strings from breaking layout */
|
||||
.ant-btn > span,
|
||||
.ant-menu-title-content,
|
||||
.ant-select-selection-item,
|
||||
.ant-form-item-label > label,
|
||||
.ant-table-cell,
|
||||
.ant-tag {
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.ant-form-item-label > label {
|
||||
white-space: normal !important;
|
||||
height: auto !important;
|
||||
line-height: 1.4 !important;
|
||||
}
|
||||
|
||||
.ant-menu-item,
|
||||
.ant-menu-submenu-title {
|
||||
white-space: normal !important;
|
||||
height: auto !important;
|
||||
line-height: 1.35 !important;
|
||||
padding-block: 10px !important;
|
||||
}
|
||||
|
||||
.ant-menu-item .ant-menu-title-content,
|
||||
.ant-menu-submenu-title .ant-menu-title-content {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
/* Desktop readability */
|
||||
@media (min-width: 768px) {
|
||||
.app-page-content {
|
||||
max-width: 1400px;
|
||||
width: 100%;
|
||||
margin-inline: auto !important;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.page-header h3.ant-typography {
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.page-header .page-header-subtitle {
|
||||
max-width: 720px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.ant-card {
|
||||
border-radius: 10px;
|
||||
}
|
||||
.ant-table-wrapper {
|
||||
border-radius: 8px;
|
||||
}
|
||||
/* Comfortable row density */
|
||||
.ant-table-thead > tr > th {
|
||||
font-weight: 600;
|
||||
}
|
||||
.ant-card + .ant-card {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
/* Desktop action rows stay usable with long labels */
|
||||
.page-toolbar,
|
||||
.ant-card-extra {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
/* iOS zoom prevention on focus */
|
||||
input.ant-input,
|
||||
.ant-input,
|
||||
.ant-input-number-input,
|
||||
@@ -47,8 +97,9 @@ body {
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
background: color-mix(in srgb, var(--ant-color-bg-container, #fff) 88%, transparent) !important;
|
||||
border-bottom: 1px solid var(--ant-color-border-secondary, rgba(0,0,0,.06));
|
||||
border-bottom: 1px solid var(--ant-color-border-secondary, rgba(0, 0, 0, 0.06));
|
||||
height: 52px !important;
|
||||
line-height: 52px !important;
|
||||
padding-inline: 8px !important;
|
||||
@@ -57,6 +108,15 @@ body {
|
||||
min-width: 40px;
|
||||
min-height: 40px;
|
||||
}
|
||||
.app-header-bar .ant-space {
|
||||
flex-wrap: nowrap;
|
||||
gap: 4px !important;
|
||||
}
|
||||
.app-header-bar .ant-tag {
|
||||
max-width: 96px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.app-page-content {
|
||||
margin: 8px !important;
|
||||
@@ -64,6 +124,30 @@ body {
|
||||
border-radius: 12px !important;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 12px !important;
|
||||
}
|
||||
.page-header .page-header-title-row {
|
||||
flex-direction: column;
|
||||
align-items: stretch !important;
|
||||
gap: 8px;
|
||||
}
|
||||
.page-header .page-header-actions {
|
||||
width: 100%;
|
||||
}
|
||||
.page-header .page-header-actions .ant-space {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.page-header .page-header-actions .ant-btn {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.page-header .page-header-subtitle {
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.ant-table-wrapper {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
@@ -98,6 +182,7 @@ body {
|
||||
}
|
||||
.ant-modal-footer .ant-btn {
|
||||
margin: 0 !important;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.ant-card {
|
||||
@@ -108,6 +193,11 @@ body {
|
||||
min-height: auto;
|
||||
padding: 10px 12px !important;
|
||||
}
|
||||
.ant-card-head-title {
|
||||
white-space: normal !important;
|
||||
padding: 4px 0 !important;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.ant-card-head-wrapper {
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
@@ -118,36 +208,42 @@ body {
|
||||
}
|
||||
.ant-card-extra .ant-space {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.ant-card-body {
|
||||
padding: 12px !important;
|
||||
}
|
||||
|
||||
.ant-space {
|
||||
flex-wrap: wrap !important;
|
||||
}
|
||||
|
||||
.ant-card-extra .ant-input-search,
|
||||
.ant-card-extra .ant-input-affix-wrapper {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.ant-tabs-nav {
|
||||
margin-bottom: 8px !important;
|
||||
margin-bottom: 12px !important;
|
||||
}
|
||||
.ant-tabs-tab {
|
||||
padding: 8px 10px !important;
|
||||
}
|
||||
.ant-tabs-tab-btn {
|
||||
white-space: normal;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ant-form-item {
|
||||
margin-bottom: 12px !important;
|
||||
}
|
||||
.ant-form-item-control-input-content > .ant-input,
|
||||
.ant-form-item-control-input-content > .ant-input-password,
|
||||
.ant-form-item-control-input-content > .ant-input-number,
|
||||
.ant-form-item-control-input-content > .ant-select,
|
||||
.ant-form-item-control-input-content > .ant-picker {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.ant-statistic-content {
|
||||
font-size: 20px !important;
|
||||
}
|
||||
.ant-statistic-title {
|
||||
white-space: normal;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.settings-section-select .ant-select-selector {
|
||||
min-height: 44px !important;
|
||||
@@ -168,6 +264,7 @@ body {
|
||||
|
||||
.ant-dropdown-menu-item {
|
||||
padding: 10px 14px !important;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.login-page {
|
||||
@@ -180,11 +277,14 @@ body {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.ant-btn-primary {
|
||||
.ant-btn {
|
||||
min-height: 36px;
|
||||
}
|
||||
.ant-btn-primary {
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
/* Entity lists */
|
||||
/* Entity lists (users / listeners mobile cards) */
|
||||
.mobile-entity-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -194,7 +294,7 @@ body {
|
||||
border: 1px solid var(--ant-color-border-secondary, #f0f0f0);
|
||||
border-radius: 12px;
|
||||
background: var(--ant-color-bg-container, #fff);
|
||||
padding: 10px 12px;
|
||||
padding: 12px;
|
||||
}
|
||||
.mobile-entity-card-main {
|
||||
display: flex;
|
||||
@@ -215,7 +315,7 @@ body {
|
||||
}
|
||||
.mobile-entity-sub {
|
||||
font-weight: 400;
|
||||
opacity: 0.65;
|
||||
opacity: 0.7;
|
||||
font-size: 13px;
|
||||
}
|
||||
.mobile-entity-meta {
|
||||
@@ -223,14 +323,48 @@ body {
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-top: 6px;
|
||||
margin-top: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.mobile-entity-rows {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
margin-top: 8px;
|
||||
font-size: 12.5px;
|
||||
line-height: 1.4;
|
||||
opacity: 0.85;
|
||||
}
|
||||
.mobile-entity-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: space-between;
|
||||
min-width: 0;
|
||||
}
|
||||
.mobile-entity-row .label {
|
||||
opacity: 0.65;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.mobile-entity-row .value {
|
||||
text-align: end;
|
||||
word-break: break-word;
|
||||
min-width: 0;
|
||||
}
|
||||
.mobile-empty {
|
||||
text-align: center;
|
||||
padding: 32px 12px;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
/* Drawer nav: comfortable touch targets + wrapping labels */
|
||||
.app-mobile-drawer .ant-menu-item {
|
||||
margin-block: 2px !important;
|
||||
width: calc(100% - 8px) !important;
|
||||
}
|
||||
|
||||
/* Stats grids */
|
||||
.ant-row {
|
||||
row-gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
@@ -238,4 +372,13 @@ body {
|
||||
margin: 6px !important;
|
||||
padding: 10px !important;
|
||||
}
|
||||
.ant-card-body {
|
||||
padding: 10px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark mode soft adjustments */
|
||||
html.dark .mobile-entity-card,
|
||||
[data-theme='dark'] .mobile-entity-card {
|
||||
border-color: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
Reference in new issue
Block a user