feat: TextInput/TextArea borderless+ariaLabel+onKeyDown, Alert info, иконка info (0.11.0)

Для инлайн-ячеек таблиц СТ и info-баннеров при перекраске форм.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Тимур Городецкий
2026-08-26 08:39:25 +05:00
parent fa6fe8dbe1
commit f2e8108d1b
9 changed files with 66 additions and 8 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@tnsu/ui-kit", "name": "@tnsu/ui-kit",
"version": "0.10.0", "version": "0.11.0",
"description": "TNSU shared UI components for SPFx web parts", "description": "TNSU shared UI components for SPFx web parts",
"author": "Timur Gorodetskiy", "author": "Timur Gorodetskiy",
"license": "ISC", "license": "ISC",
+6
View File
@@ -28,3 +28,9 @@
color: $red-500; color: $red-500;
border-color: #fecaca; border-color: #fecaca;
} }
.info {
background: $navy-50;
color: $navy-800;
border-color: $navy-100;
}
+5 -3
View File
@@ -3,7 +3,7 @@ import styles from './Alert.module.scss';
import { Icon } from '../Icon/Icon'; import { Icon } from '../Icon/Icon';
import { IconName } from '../Icon/iconNames'; import { IconName } from '../Icon/iconNames';
type AlertVariant = 'warn' | 'ok' | 'err'; type AlertVariant = 'warn' | 'ok' | 'err' | 'info';
interface IAlertProps { interface IAlertProps {
variant: AlertVariant; variant: AlertVariant;
@@ -13,13 +13,15 @@ interface IAlertProps {
const variantClass: Record<AlertVariant, string> = { const variantClass: Record<AlertVariant, string> = {
warn: styles.warn, warn: styles.warn,
ok: styles.ok, ok: styles.ok,
err: styles.err err: styles.err,
info: styles.info
}; };
const variantIcon: Record<AlertVariant, IconName> = { const variantIcon: Record<AlertVariant, IconName> = {
warn: 'warning', warn: 'warning',
ok: 'check', ok: 'check',
err: 'close' err: 'close',
info: 'info'
}; };
export const Alert: React.FC<IAlertProps> = ({ variant, children }) => { export const Alert: React.FC<IAlertProps> = ({ variant, children }) => {
+4
View File
@@ -147,6 +147,10 @@ export const IconSprite: React.FC = () => (
<path d="M12 2 1 21h22z" strokeLinejoin="round" /> <path d="M12 2 1 21h22z" strokeLinejoin="round" />
<path d="M12 9v5M12 17h.01" /> <path d="M12 9v5M12 17h.01" />
</symbol> </symbol>
<symbol id="i-info" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="9" />
<path d="M12 11v5M12 7.5h.01" />
</symbol>
<symbol id="i-check" viewBox="0 0 24 24"> <symbol id="i-check" viewBox="0 0 24 24">
<path d="M5 12l5 5L20 7" /> <path d="M5 12l5 5L20 7" />
</symbol> </symbol>
+1
View File
@@ -35,6 +35,7 @@ export const ICON_NAMES = [
'search', 'search',
'menu', 'menu',
'warning', 'warning',
'info',
'check', 'check',
'plus' 'plus'
] as const; ] as const;
+14
View File
@@ -21,3 +21,17 @@
color: $gray-400; color: $gray-400;
} }
} }
// Инлайн-ввод в ячейке таблицы: без рамки/фона, компактный, без ручного ресайза.
.borderless {
border-color: transparent;
background: transparent;
padding: 6px 8px;
min-height: 32px;
resize: none;
&:focus {
background: $white;
border-color: $gold-500;
}
}
+7 -2
View File
@@ -10,20 +10,25 @@ interface ITextAreaProps {
rows?: number; rows?: number;
/** Только чтение (режим согласующего). */ /** Только чтение (режим согласующего). */
readOnly?: boolean; readOnly?: boolean;
/** Без рамки/фона — для инлайн-ввода в ячейках таблиц. */
borderless?: boolean;
/** a11y-подпись, когда видимого label нет. */
ariaLabel?: string;
} }
/** Многострочный ввод (SharePoint Note). */ /** Многострочный ввод (SharePoint Note). */
export const TextArea: React.FC<ITextAreaProps> = ({ id, value, onChange, placeholder, rows = 3, readOnly }) => { export const TextArea: React.FC<ITextAreaProps> = ({ id, value, onChange, placeholder, rows = 3, readOnly, borderless, ariaLabel }) => {
const contextReadOnly = useFormReadOnly(); const contextReadOnly = useFormReadOnly();
const effectiveReadOnly = readOnly ?? contextReadOnly; const effectiveReadOnly = readOnly ?? contextReadOnly;
return ( return (
<textarea <textarea
id={id} id={id}
className={styles.textarea} className={[styles.textarea, borderless ? styles.borderless : ''].filter(Boolean).join(' ')}
value={value} value={value}
placeholder={placeholder} placeholder={placeholder}
rows={rows} rows={rows}
readOnly={effectiveReadOnly} readOnly={effectiveReadOnly}
aria-label={ariaLabel}
onChange={(e) => onChange(e.target.value)} onChange={(e) => onChange(e.target.value)}
/> />
); );
+13
View File
@@ -19,3 +19,16 @@
color: $gray-400; color: $gray-400;
} }
} }
// Инлайн-ввод в ячейке таблицы: без рамки/фона, компактнее; рамка/фон проявляются на фокусе.
.borderless {
border-color: transparent;
background: transparent;
padding: 6px 8px;
border-radius: 4px;
&:focus {
background: $white;
border-color: $gold-500;
}
}
+15 -2
View File
@@ -19,6 +19,12 @@ interface ITextInputProps {
*/ */
min?: string | number; min?: string | number;
max?: string | number; max?: string | number;
/** Без рамки/фона — для инлайн-ввода в ячейках таблиц (рамка проявляется на фокусе). */
borderless?: boolean;
/** a11y-подпись, когда видимого label нет (ячейки таблиц). */
ariaLabel?: string;
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
autoFocus?: boolean;
} }
/** Однострочный ввод. type='number'|'date' — для числовых полей и дат. */ /** Однострочный ввод. type='number'|'date' — для числовых полей и дат. */
@@ -30,20 +36,27 @@ export const TextInput: React.FC<ITextInputProps> = ({
placeholder, placeholder,
readOnly, readOnly,
min, min,
max max,
borderless,
ariaLabel,
onKeyDown,
autoFocus
}) => { }) => {
const contextReadOnly = useFormReadOnly(); const contextReadOnly = useFormReadOnly();
const effectiveReadOnly = readOnly ?? contextReadOnly; const effectiveReadOnly = readOnly ?? contextReadOnly;
return ( return (
<input <input
id={id} id={id}
className={styles.input} className={[styles.input, borderless ? styles.borderless : ''].filter(Boolean).join(' ')}
type={type} type={type}
value={value} value={value}
placeholder={placeholder} placeholder={placeholder}
readOnly={effectiveReadOnly} readOnly={effectiveReadOnly}
aria-label={ariaLabel}
min={min} min={min}
max={max} max={max}
autoFocus={autoFocus}
onKeyDown={onKeyDown}
onChange={(e) => onChange(e.target.value)} onChange={(e) => onChange(e.target.value)}
/> />
); );