UI: Add Island and Surface (#6851)

This commit is contained in:
zubiden 2026-04-17 13:38:13 +02:00 committed by Alexander Zinchuk
parent 7e789e6e77
commit 83ed53d86d
11 changed files with 295 additions and 37 deletions

View File

@ -1,4 +1,4 @@
@layer ui.input {
@layer ui.layout {
.control {
display: grid;
grid-template-areas: "input label";

View File

@ -1,4 +1,4 @@
@layer ui.input {
@layer ui.layout {
.interactive {
position: relative;

View File

@ -0,0 +1,38 @@
@layer ui.layout {
.island {
padding: 0.5rem;
border-radius: var(--border-radius-island);
background-color: var(--color-background);
box-shadow: 0px 1px 4px 0px #0000000D;
}
.description {
display: block;
padding: 0.5rem 1rem;
font-size: 0.875rem;
line-height: 1rem;
color: var(--color-text-secondary);
overflow-wrap: anywhere;
}
.text {
display: block;
padding: 0.5rem 1rem;
line-height: 1.25rem;
overflow-wrap: anywhere;
}
.island + .island {
margin-top: 1rem;
}
.island + .description {
margin-top: 0.5rem;
}
.description + .island {
margin-top: 1rem;
}
}

View File

@ -0,0 +1,46 @@
import buildClassName from '../../../util/buildClassName';
import styles from './Island.module.scss';
type OwnProps = React.HTMLAttributes<HTMLDivElement> & {
children: React.ReactNode;
};
const Island = ({ className, children, ...otherProps }: OwnProps) => {
return (
<div
className={buildClassName(styles.island, className)}
{...otherProps}
>
{children}
</div>
);
};
const IslandDescription = ({ className, children, ...otherProps }: OwnProps) => {
return (
<div
className={buildClassName(styles.description, className)}
{...otherProps}
>
{children}
</div>
);
};
const IslandText = ({ className, children, ...otherProps }: OwnProps) => {
return (
<div
className={buildClassName(styles.text, className)}
{...otherProps}
>
{children}
</div>
);
};
export default Island;
export {
IslandDescription,
IslandText,
};

View File

@ -0,0 +1,15 @@
@use '../../../styles/mixins';
@layer ui.layout {
.root {
padding-inline: 1rem;
background-color: var(--color-background-secondary);
}
.scrollable {
scrollbar-gutter: stable;
overflow-y: auto;
@include mixins.adapt-padding-to-scrollbar(1rem);
}
}

View File

@ -0,0 +1,33 @@
import buildClassName from '../../../util/buildClassName';
import styles from './Surface.module.scss';
type OwnProps = React.HTMLAttributes<HTMLDivElement> & {
scrollable?: boolean;
children: React.ReactNode;
};
const Surface = ({
scrollable,
className,
children,
...otherProps
}: OwnProps) => {
const isScrollable = Boolean(scrollable);
return (
<div
className={buildClassName(
styles.root,
isScrollable && 'custom-scroll',
isScrollable && styles.scrollable,
className,
)}
{...otherProps}
>
{children}
</div>
);
};
export default Surface;

View File

@ -0,0 +1,81 @@
.root {
height: 100vh;
}
.content {
columns: 28rem 2;
column-gap: 2rem;
padding-block: 2rem;
}
.fullWidth {
column-span: all;
break-inside: avoid;
}
.title {
margin: 0 0 1.5rem;
}
.layoutPreview,
.section {
break-inside: avoid;
margin-bottom: 2rem;
}
.layoutPreview {
display: flex;
flex-direction: column;
}
.sectionTitle {
margin: 0 0 0.5rem;
font-size: 0.875rem;
font-weight: var(--font-weight-medium);
color: var(--color-text-secondary);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.sectionContent {
overflow: hidden;
border: 1px solid var(--color-borders-input);
border-radius: 0.75rem;
}
.sectionContentNoBorder {
border: 0;
border-radius: 0;
}
.barePrimitives {
display: flex;
flex-wrap: wrap;
gap: 1rem;
align-items: center;
padding: 1rem;
}
.previewCard {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.previewLabel {
font-size: 0.75rem;
font-weight: var(--font-weight-semibold);
color: var(--color-text-secondary);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.previewText {
line-height: 1.25rem;
}
.bareControl {
padding: 0.5rem 1rem;
}

View File

@ -1,40 +1,48 @@
/* eslint-disable @stylistic/max-len */
import { useState } from '../../lib/teact/teact';
import { useState } from '../../../lib/teact/teact';
import buildStyle from '../../util/buildStyle';
import buildClassName from '../../../util/buildClassName';
import Control, {
ControlAfter,
ControlBefore,
ControlDescription,
ControlLabel,
} from '../gili/layout/Control';
import Interactive from '../gili/layout/Interactive';
import Checkbox from '../gili/primitives/Checkbox';
import Radio from '../gili/primitives/Radio';
import Switch from '../gili/primitives/Switch';
import CheckboxField from '../gili/templates/CheckboxField';
import SwitchField from '../gili/templates/SwitchField';
} from '../../gili/layout/Control';
import Interactive from '../../gili/layout/Interactive';
import Island, {
IslandDescription,
IslandText,
} from '../../gili/layout/Island';
import Surface from '../../gili/layout/Surface';
import Checkbox from '../../gili/primitives/Checkbox';
import Radio from '../../gili/primitives/Radio';
import Switch from '../../gili/primitives/Switch';
import CheckboxField from '../../gili/templates/CheckboxField';
import SwitchField from '../../gili/templates/SwitchField';
function Section({ title, children, noBorder }: { title: string; children: any; noBorder?: boolean }) {
import styles from './FieldDemo.module.scss';
type SectionProps = {
title: string;
children: React.ReactNode;
noBorder?: boolean;
};
function Section({ title, children, noBorder }: SectionProps) {
return (
<div style="margin-bottom: 2rem; break-inside: avoid">
<h3 style="margin: 0 0 0.5rem; font-size: 0.875rem; color: #888; text-transform: uppercase; letter-spacing: 0.05em">
{title}
</h3>
<div
style={buildStyle(
!noBorder && 'border: 1px solid var(--color-borders-input); border-radius: 0.75rem',
'overflow: hidden',
)}
>
{children}
</div>
<div className={styles.section}>
<Island>
<h3 className={styles.sectionTitle}>{title}</h3>
<div className={buildClassName(styles.sectionContent, noBorder && styles.sectionContentNoBorder)}>
{children}
</div>
</Island>
</div>
);
}
const FieldTest = () => {
const FieldDemo = () => {
const [check1, setCheck1] = useState(false);
const [check2, setCheck2] = useState(true);
const [check3, setCheck3] = useState(false);
@ -65,13 +73,50 @@ const FieldTest = () => {
const [switch4, setSwitch4] = useState(true);
return (
<div style="overflow-y: auto; height: 100vh">
<div style="columns: 28rem 2; column-gap: 2rem; padding: 2rem">
<h2 style="margin: 0 0 1.5rem; column-span: all">Control Component Test</h2>
<Surface className={styles.root} scrollable>
<div className={styles.content}>
<h2 className={buildClassName(styles.title, styles.fullWidth)}>Control Component Test</h2>
<div className={buildClassName(styles.layoutPreview, styles.fullWidth)}>
<Island>
<IslandText>
<h3 className={styles.sectionTitle}>Surface + Islands</h3>
<div className={styles.previewCard}>
<span className={styles.previewLabel}>Island</span>
<span className={styles.previewText}>
Regular background, island radius, and 0.5rem padding.
</span>
</div>
</IslandText>
</Island>
<IslandDescription>
IslandDescription stays attached to the island above it, and the next island starts 1rem lower.
</IslandDescription>
<Island>
<IslandText>
<div className={styles.previewCard}>
<span className={styles.previewLabel}>Island After Description</span>
<span className={styles.previewText}>
This island verifies the description-to-island spacing rule.
</span>
</div>
</IslandText>
</Island>
<Island>
<IslandText>
<div className={styles.previewCard}>
<span className={styles.previewLabel}>Island After Island</span>
<span className={styles.previewText}>
This island verifies the direct island-to-island 1rem gap.
</span>
</div>
</IslandText>
</Island>
</div>
{/* Bare primitives */}
<Section title="Bare Primitives (no Control)">
<div style="display: flex; gap: 1rem; padding: 1rem; align-items: center">
<div className={styles.barePrimitives}>
<Checkbox checked={check1} onChange={setCheck1} />
<Checkbox checked={check2} onChange={setCheck2} />
<Checkbox checked={false} isInvalid onChange={setCheck1} />
@ -481,7 +526,7 @@ const FieldTest = () => {
{/* Control without Interactive */}
<Section noBorder title="Control without Interactive (no padding/hover)">
<div style="padding: 0.5rem 1rem">
<div className={styles.bareControl}>
<Control>
<Checkbox checked={check2} onChange={setCheck2} />
<ControlLabel>Bare field, custom container</ControlLabel>
@ -494,8 +539,8 @@ const FieldTest = () => {
</div>
</Section>
</div>
</div>
</Surface>
);
};
export default FieldTest;
export default FieldDemo;

View File

@ -1,9 +1,9 @@
import type { FormatDateTimeOptions } from '../../util/localization/dateFormat';
import type { FormatDateTimeOptions } from '../../../util/localization/dateFormat';
import buildClassName from '../../util/buildClassName';
import { formatDateTime, formatMessageListDate } from '../../util/localization/dateFormat';
import buildClassName from '../../../util/buildClassName';
import { formatDateTime, formatMessageListDate } from '../../../util/localization/dateFormat';
import useLang from '../../hooks/useLang';
import useLang from '../../../hooks/useLang';
import styles from './TestDateFormat.module.scss';

View File

@ -54,7 +54,7 @@
<style>
@layer reset, variables, ui, components;
@layer ui {
@layer tablist, spinner, button, input;
@layer tablist, spinner, button, input, layout;
}
</style>
</head>