fix(admin-ui): IconButton icon prop + compact 2-col form layout

Bugs:
- IconButton @nstart/ui принимает icon={} prop, не children — иконки рендерились
  пустыми квадратами (видно «Изменить» tooltip без значка карандашика).
  Перевёл оба IconButton на icon={<PencilSimpleIcon/>} / icon={<XCircleIcon/>}.

UX:
- Modal max-w-2xl → max-w-4xl (форма дышит, ru-RU/en-US labels не оборачиваются).
- SchemaDrivenForm: scalar поля теперь в 2-колоночной grid, full-width
  (i18n / array / textarea / nested object) занимают sm:col-span-2.
- Локальные ярлыки i18n: tracking-normal + whitespace-nowrap, w-14 — больше не
  ломаются «ru-» «RU» из-за inherited tracking-label.
This commit is contained in:
Zimin A.N.
2026-05-04 02:37:13 +03:00
parent 4f04872de8
commit 7aed67fce5
2 changed files with 41 additions and 26 deletions
@@ -96,9 +96,10 @@ export const SchemaDrivenForm = ({
} }
return ( return (
<form onSubmit={handleSubmit(submit)} className="space-y-6"> <form onSubmit={handleSubmit(submit)} className="space-y-4">
<FormSection title={t('form.identity')}> <FormSection title={t('form.identity')}>
<FormGrid columns={1}> <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div className="sm:col-span-2">
<TextInput <TextInput
label={t('form.businessKey')} label={t('form.businessKey')}
required required
@@ -107,8 +108,7 @@ export const SchemaDrivenForm = ({
error={formState.errors.businessKey ? t('form.error.required') : undefined} error={formState.errors.businessKey ? t('form.error.required') : undefined}
{...register('businessKey', { required: true, minLength: 1 })} {...register('businessKey', { required: true, minLength: 1 })}
/> />
</FormGrid> </div>
<FormGrid columns={2}>
<TextInput <TextInput
type="datetime-local" type="datetime-local"
label={t('form.validFrom')} label={t('form.validFrom')}
@@ -119,11 +119,11 @@ export const SchemaDrivenForm = ({
label={t('form.validTo')} label={t('form.validTo')}
{...register('validTo')} {...register('validTo')}
/> />
</FormGrid> </div>
</FormSection> </FormSection>
<FormSection title={t('form.dataFields')}> <FormSection title={t('form.dataFields')}>
<FormGrid columns={1}> <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{Object.entries(properties).map(([key, propSchema]) => ( {Object.entries(properties).map(([key, propSchema]) => (
<FieldRenderer <FieldRenderer
key={key} key={key}
@@ -136,7 +136,7 @@ export const SchemaDrivenForm = ({
fieldError={formState.errors.data?.[key as keyof FormValues['data']]} fieldError={formState.errors.data?.[key as keyof FormValues['data']]}
/> />
))} ))}
</FormGrid> </div>
</FormSection> </FormSection>
{serverError && <Alert variant="error">{serverError}</Alert>} {serverError && <Alert variant="error">{serverError}</Alert>}
@@ -153,6 +153,14 @@ export const SchemaDrivenForm = ({
) )
} }
const isFullWidth = (s: JsonSchema): boolean => {
if (s['x-localized']) return true
if (s.type === 'array') return true
if ((s.maxLength ?? 0) > 200) return true
if (s.type === 'object') return true
return false
}
type FieldRendererProps = { type FieldRendererProps = {
fieldKey: string fieldKey: string
schema: JsonSchema schema: JsonSchema
@@ -163,7 +171,16 @@ type FieldRendererProps = {
fieldError?: unknown fieldError?: unknown
} }
const FieldRenderer = ({ const FieldRenderer = (props: FieldRendererProps) => {
const wrapperClass = isFullWidth(props.schema) ? 'sm:col-span-2' : ''
return (
<div className={wrapperClass}>
<FieldBody {...props} />
</div>
)
}
const FieldBody = ({
fieldKey, fieldKey,
schema, schema,
required, required,
@@ -188,11 +205,11 @@ const FieldRenderer = ({
control={control} control={control}
name={`data.${fieldKey}.${loc}` as `data.${string}`} name={`data.${fieldKey}.${loc}` as `data.${string}`}
render={({ field }) => ( render={({ field }) => (
<div className="flex items-center gap-2"> <div className="flex items-center gap-3">
<span className="text-2xs font-mono text-carbon/70 w-12 shrink-0"> <span className="text-xs font-mono text-carbon/70 normal-case tracking-normal whitespace-nowrap shrink-0 w-14">
{loc === defaultLocale ? <strong>{loc}</strong> : loc} {loc === defaultLocale ? <strong>{loc}</strong> : loc}
</span> </span>
<div className="flex-1"> <div className="flex-1 min-w-0">
<TextInput {...field} value={(field.value as string | undefined) ?? ''} /> <TextInput {...field} value={(field.value as string | undefined) ?? ''} />
</div> </div>
</div> </div>
@@ -156,17 +156,15 @@ function DictionaryDetail() {
<IconButton <IconButton
label={t('dict.action.edit')} label={t('dict.action.edit')}
variant="default" variant="default"
icon={<PencilSimpleIcon weight="regular" />}
onClick={() => setEdit({ kind: 'edit', record: r })} onClick={() => setEdit({ kind: 'edit', record: r })}
> />
<PencilSimpleIcon size={16} />
</IconButton>
<IconButton <IconButton
label={t('dict.action.close')} label={t('dict.action.close')}
variant="danger" variant="danger"
icon={<XCircleIcon weight="regular" />}
onClick={() => setEdit({ kind: 'close-confirm', record: r })} onClick={() => setEdit({ kind: 'close-confirm', record: r })}
> />
<XCircleIcon size={16} />
</IconButton>
</div> </div>
</TableCell> </TableCell>
</TableRow> </TableRow>
@@ -186,7 +184,7 @@ function DictionaryDetail() {
? `${t('dict.action.edit')}: ${edit.record.businessKey}` ? `${t('dict.action.edit')}: ${edit.record.businessKey}`
: '' : ''
} }
maxWidth="max-w-2xl" maxWidth="max-w-4xl"
> >
{detailQuery.data && (edit.kind === 'create' || edit.kind === 'edit') && ( {detailQuery.data && (edit.kind === 'create' || edit.kind === 'edit') && (
<SchemaDrivenForm <SchemaDrivenForm