@asteby/metacore-runtime-react 18.15.0 → 18.16.1

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.
@@ -3,16 +3,18 @@
3
3
  // Transport-agnostic: every read/write arrives via props (loaders/mutators),
4
4
  // so each host wires them to its own api client (ops → /api/permissions/*).
5
5
  // The capability universe (modules × actions + general flags) is derived from
6
- // the installed manifests server-side; this component only renders it.
6
+ // the installed manifests + the real sidebar nav server/host-side; this
7
+ // component only renders it.
7
8
  //
8
- // Layout (mirrors the app sidebar so admins recognise what they grant):
9
+ // Layout — a *flat list* that mirrors the app sidebar (NO accordions/folders):
9
10
  // header — title + "Nuevo rol" (primary) + "Guardar permisos" (green).
10
11
  // left — Card "Rol": clean role combobox with inline Editar/Eliminar
11
12
  // icons (no removable chip) + "Permisos Generales" flags.
12
- // — Card "Módulos": a searchable, accordion *tree* grouped by
13
- // `addon_label` (modules without one "Sistema"). Each group
14
- // lists its modules with their icon + a granted-count badge.
15
- // Clicking a module selects it and reveals its action grid.
13
+ // — Card "Módulos": a searchable flat list. Each group renders a
14
+ // non-collapsible grey header (uppercase tracking, like "Módulos"
15
+ // / "Sistema" in the sidebar) followed by its modules as clickable
16
+ // rows (icon + label + granted-count badge). Clicking a row selects
17
+ // that module and reveals its action grid on the right.
16
18
  // right — Card "Acciones permitidas": granted counter N/M, mark-all /
17
19
  // clear, checkbox grid (icon + label per action). Clear empty
18
20
  // states for "pick a role" / "pick a module" / loading.
@@ -24,11 +26,9 @@
24
26
  import * as React from 'react'
25
27
  import {
26
28
  Check,
27
- ChevronRight,
28
29
  ChevronsUpDown,
29
30
  CheckCheck,
30
31
  Eraser,
31
- Folder,
32
32
  Pencil,
33
33
  Plus,
34
34
  Save,
@@ -55,9 +55,6 @@ import {
55
55
  CardHeader,
56
56
  CardTitle,
57
57
  Checkbox,
58
- Collapsible,
59
- CollapsibleContent,
60
- CollapsibleTrigger,
61
58
  Command,
62
59
  CommandEmpty,
63
60
  CommandGroup,
@@ -80,34 +77,52 @@ import {
80
77
  import { DynamicIcon } from './dynamic-icon'
81
78
 
82
79
  // ---------------------------------------------------------------------------
83
- // Types (mirror of `GET /api/permissions/modules` / `/api/permissions/roles`)
80
+ // Types (mirror of `GET /api/permissions/modules` + the host sidebar nav)
84
81
  // ---------------------------------------------------------------------------
85
82
 
86
83
  export interface PermissionActionDef {
87
- /** Canonical action key (`index`, `create`, …, or a custom key like `pagar`). */
84
+ /** Canonical action key (`index`, `create`, …, a custom `pagar`, or `access`). */
88
85
  key: string
89
- /** Localized label ("Listar", "Pagar"). */
86
+ /** Localized label ("Listar", "Pagar", "Acceder"). */
90
87
  label: string
91
88
  /** Lucide icon name from the manifest action (optional). */
92
89
  icon?: string
93
- /** `crud` for the derived CRUD set, `custom` for manifest actions. */
94
- kind?: 'crud' | 'custom' | string
90
+ /**
91
+ * `crud` for the derived CRUD set, `custom` for manifest actions,
92
+ * `screen` for the single `access` action of a non-model screen.
93
+ */
94
+ kind?: 'crud' | 'custom' | 'screen' | string
95
95
  }
96
96
 
97
97
  export interface PermissionModuleDef {
98
- /** Module key = lowercase model table (`pos_orders`). */
98
+ /**
99
+ * Module key.
100
+ * - model: lowercase model table (`pos_orders`).
101
+ * - screen: `screen.<navKey>` (the host prefixes it).
102
+ */
99
103
  key: string
100
- /** Localized module label ("Pedidos POS"). */
104
+ /** Localized module label ("Pedidos POS", "Terminal"). */
101
105
  label: string
102
106
  /** Module icon (lucide name) — mirrors the sidebar entry. */
103
107
  icon?: string
104
- /** Owning addon key (`pos`). */
108
+ /** Whether this entry is a data model or a non-model screen. */
109
+ kind?: 'model' | 'screen'
110
+ /** Owning addon key (`pos`) — legacy shape only, used for grouping. */
105
111
  addon_key?: string
106
- /** Localized addon label ("Punto de venta") — used to group the tree. */
112
+ /** Localized addon label ("Punto de venta") — legacy shape only. */
107
113
  addon_label?: string
108
114
  actions: PermissionActionDef[]
109
115
  }
110
116
 
117
+ /**
118
+ * A sidebar-style group: a grey (non-collapsible) header + its modules.
119
+ * `title === ''` → no header (e.g. core/infra modules).
120
+ */
121
+ export interface ModuleGroup {
122
+ title: string
123
+ modules: PermissionModuleDef[]
124
+ }
125
+
111
126
  export interface GeneralPermissionDef {
112
127
  /** Full capability key (`general.work_after_hours`). */
113
128
  key: string
@@ -115,11 +130,27 @@ export interface GeneralPermissionDef {
115
130
  description?: string
116
131
  }
117
132
 
118
- export interface PermissionsCatalog {
133
+ /**
134
+ * What `loadModules()` may return.
135
+ *
136
+ * New (preferred) shape — pre-grouped flat list, mirrors the host sidebar:
137
+ * { groups: ModuleGroup[], general }
138
+ *
139
+ * Legacy shape (still accepted, wrapped into a single untitled group) —
140
+ * { modules: PermissionModuleDef[], general }
141
+ */
142
+ export interface GroupedPermissionsCatalog {
143
+ groups: ModuleGroup[]
144
+ general: GeneralPermissionDef[]
145
+ }
146
+
147
+ export interface FlatPermissionsCatalog {
119
148
  modules: PermissionModuleDef[]
120
149
  general: GeneralPermissionDef[]
121
150
  }
122
151
 
152
+ export type PermissionsCatalog = GroupedPermissionsCatalog | FlatPermissionsCatalog
153
+
123
154
  export interface RoleDef {
124
155
  id: string
125
156
  /** Stable role key ("cashier"). */
@@ -137,7 +168,7 @@ export interface RoleInput {
137
168
  }
138
169
 
139
170
  export interface PermissionsManagerProps {
140
- /** Loads the module×action universe + general flags. */
171
+ /** Loads the module×action universe + general flags (grouped or flat). */
141
172
  loadModules: () => Promise<PermissionsCatalog>
142
173
  /** Loads every assignable role. */
143
174
  loadRoles: () => Promise<RoleDef[]>
@@ -197,19 +228,23 @@ export function defaultActionIcon(actionKey: string, kind?: string): string {
197
228
  return 'Download'
198
229
  case 'import':
199
230
  return 'Upload'
231
+ case 'access':
232
+ return 'Eye'
200
233
  default:
201
- return kind === 'crud' ? 'List' : 'Zap'
234
+ if (kind === 'crud') return 'List'
235
+ if (kind === 'screen') return 'Eye'
236
+ return 'Zap'
202
237
  }
203
238
  }
204
239
 
205
- /** Group label fallback when a module has no addon ("Sistema" = core/infra). */
240
+ /** Group label fallback when a legacy module has no addon ("Sistema" = core). */
206
241
  const SYSTEM_GROUP = 'Sistema'
207
242
 
208
- function moduleGroupLabel(mod: PermissionModuleDef): string {
243
+ function legacyGroupLabel(mod: PermissionModuleDef): string {
209
244
  return mod.addon_label || mod.addon_key || SYSTEM_GROUP
210
245
  }
211
246
 
212
- /** Accent-insensitive, lowercase fold for tree search. */
247
+ /** Accent-insensitive, lowercase fold for search. */
213
248
  function fold(s: string): string {
214
249
  return s
215
250
  .normalize('NFD')
@@ -236,39 +271,59 @@ const ROLE_COLORS = [
236
271
  '#6b7280',
237
272
  ]
238
273
 
239
- /** [groupLabel, modules] buckets in stable insertion order. */
240
- export interface ModuleGroup {
241
- label: string
242
- modules: PermissionModuleDef[]
243
- }
274
+ /**
275
+ * Normalize whatever `loadModules` returned into the canonical grouped shape.
276
+ *
277
+ * - New shape (`{ groups }`): passed through (modules default to kind:'model').
278
+ * - Legacy flat shape (`{ modules }`): grouped by `addon_label`/`addon_key`
279
+ * (falling back to "Sistema") so old hosts keep their familiar buckets,
280
+ * every module defaulting to kind:'model'. The grey headers still render,
281
+ * just derived from the addon instead of the sidebar group.
282
+ */
283
+ export function normalizeCatalogGroups(catalog: PermissionsCatalog): ModuleGroup[] {
284
+ const withKind = (m: PermissionModuleDef): PermissionModuleDef => ({
285
+ ...m,
286
+ kind: m.kind ?? 'model',
287
+ })
244
288
 
245
- export function groupModules(modules: PermissionModuleDef[]): ModuleGroup[] {
289
+ if ('groups' in catalog && Array.isArray(catalog.groups)) {
290
+ return catalog.groups.map((g) => ({
291
+ title: g.title ?? '',
292
+ modules: g.modules.map(withKind),
293
+ }))
294
+ }
295
+
296
+ const modules = ('modules' in catalog && catalog.modules) || []
246
297
  const order: string[] = []
247
298
  const byGroup = new Map<string, PermissionModuleDef[]>()
248
- for (const mod of modules) {
249
- const g = moduleGroupLabel(mod)
299
+ for (const raw of modules) {
300
+ const mod = withKind(raw)
301
+ const g = legacyGroupLabel(mod)
250
302
  if (!byGroup.has(g)) {
251
303
  byGroup.set(g, [])
252
304
  order.push(g)
253
305
  }
254
306
  byGroup.get(g)!.push(mod)
255
307
  }
256
- return order.map((label) => ({ label, modules: byGroup.get(label)! }))
308
+ return order.map((title) => ({ title, modules: byGroup.get(title)! }))
309
+ }
310
+
311
+ /** Flat list of every module across groups, in render order. */
312
+ export function flattenGroups(groups: ModuleGroup[]): PermissionModuleDef[] {
313
+ return groups.flatMap((g) => g.modules)
257
314
  }
258
315
 
259
- /** Filter the grouped tree by a folded query against module + group labels. */
316
+ /** Filter the grouped flat list by a folded query against module + group titles. */
260
317
  export function filterModuleGroups(groups: ModuleGroup[], query: string): ModuleGroup[] {
261
318
  const q = fold(query).trim()
262
319
  if (!q) return groups
263
320
  const out: ModuleGroup[] = []
264
321
  for (const g of groups) {
265
- const groupMatches = fold(g.label).includes(q)
322
+ const groupMatches = g.title.length > 0 && fold(g.title).includes(q)
266
323
  const mods = groupMatches
267
324
  ? g.modules
268
- : g.modules.filter(
269
- (m) => fold(m.label).includes(q) || fold(m.key).includes(q),
270
- )
271
- if (mods.length) out.push({ label: g.label, modules: mods })
325
+ : g.modules.filter((m) => fold(m.label).includes(q) || fold(m.key).includes(q))
326
+ if (mods.length) out.push({ title: g.title, modules: mods })
272
327
  }
273
328
  return out
274
329
  }
@@ -335,8 +390,8 @@ function CapabilityCheck({
335
390
  )
336
391
  }
337
392
 
338
- /** One module row inside the tree. */
339
- function ModuleTreeItem({
393
+ /** One clickable module row in the flat list (mirrors a sidebar item). */
394
+ function ModuleRow({
340
395
  module,
341
396
  active,
342
397
  granted,
@@ -362,7 +417,7 @@ function ModuleTreeItem({
362
417
  )}
363
418
  >
364
419
  <DynamicIcon
365
- name={module.icon || 'Square'}
420
+ name={module.icon || (module.kind === 'screen' ? 'Eye' : 'Square')}
366
421
  className={cn('h-4 w-4 shrink-0', active ? 'text-primary' : 'text-muted-foreground')}
367
422
  />
368
423
  <span className="min-w-0 flex-1 truncate">{module.label}</span>
@@ -393,7 +448,8 @@ export function PermissionsManager({
393
448
  title = 'Permisos y Roles',
394
449
  className,
395
450
  }: PermissionsManagerProps) {
396
- const [catalog, setCatalog] = React.useState<PermissionsCatalog | null>(null)
451
+ const [groups, setGroups] = React.useState<ModuleGroup[] | null>(null)
452
+ const [general, setGeneral] = React.useState<GeneralPermissionDef[] | null>(null)
397
453
  const [roles, setRoles] = React.useState<RoleDef[] | null>(null)
398
454
  const [loadError, setLoadError] = React.useState(false)
399
455
 
@@ -407,9 +463,7 @@ export function PermissionsManager({
407
463
  const [saving, setSaving] = React.useState(false)
408
464
 
409
465
  const [roleOpen, setRoleOpen] = React.useState(false)
410
- const [moduleQuery, setModuleQuery] = React.useState('')
411
- // Groups the user explicitly collapsed (default: every group open).
412
- const [collapsedGroups, setCollapsedGroups] = React.useState<Set<string>>(new Set())
466
+ const [moduleOpen, setModuleOpen] = React.useState(false)
413
467
 
414
468
  // Pending role switch while there are unsaved changes.
415
469
  const [pendingRoleId, setPendingRoleId] = React.useState<string | null>(null)
@@ -424,7 +478,9 @@ export function PermissionsManager({
424
478
  const [deleteOpen, setDeleteOpen] = React.useState(false)
425
479
  const [deleting, setDeleting] = React.useState(false)
426
480
 
427
- const loading = catalog === null || roles === null
481
+ const loading = groups === null || roles === null
482
+
483
+ const allModules = React.useMemo(() => (groups ? flattenGroups(groups) : []), [groups])
428
484
 
429
485
  // ---- initial load: catalog + roles in parallel -------------------------
430
486
  React.useEffect(() => {
@@ -432,10 +488,14 @@ export function PermissionsManager({
432
488
  Promise.all([loadModules(), loadRoles()])
433
489
  .then(([cat, rs]) => {
434
490
  if (cancelled) return
435
- setCatalog(cat)
491
+ const grouped = normalizeCatalogGroups(cat)
492
+ setGroups(grouped)
493
+ setGeneral(cat.general ?? [])
436
494
  setRoles(rs)
437
495
  setActiveRoleId((prev) => prev ?? rs[0]?.id ?? null)
438
- setActiveModuleKey((prev) => prev ?? cat.modules[0]?.key ?? null)
496
+ setActiveModuleKey(
497
+ (prev) => prev ?? flattenGroups(grouped)[0]?.key ?? null,
498
+ )
439
499
  })
440
500
  .catch(() => {
441
501
  if (!cancelled) setLoadError(true)
@@ -481,20 +541,12 @@ export function PermissionsManager({
481
541
  [roles, activeRoleId],
482
542
  )
483
543
  const activeModule = React.useMemo(
484
- () => catalog?.modules.find((m) => m.key === activeModuleKey) ?? null,
485
- [catalog, activeModuleKey],
544
+ () => allModules.find((m) => m.key === activeModuleKey) ?? null,
545
+ [allModules, activeModuleKey],
486
546
  )
487
547
 
488
548
  const dirty = baseline !== null && draft !== null && !capabilitySetsEqual(baseline, draft)
489
549
 
490
- // Module tree: grouped by addon label, optionally filtered by the search.
491
- const allGroups = React.useMemo(() => groupModules(catalog?.modules ?? []), [catalog])
492
- const visibleGroups = React.useMemo(
493
- () => filterModuleGroups(allGroups, moduleQuery),
494
- [allGroups, moduleQuery],
495
- )
496
- const searching = moduleQuery.trim().length > 0
497
-
498
550
  // ---- capability edits ---------------------------------------------------
499
551
  const toggleCapability = React.useCallback((cap: string) => {
500
552
  setDraft((prev) => {
@@ -544,14 +596,6 @@ export function PermissionsManager({
544
596
  else setActiveRoleId(roleId)
545
597
  }
546
598
 
547
- const toggleGroup = (label: string) =>
548
- setCollapsedGroups((prev) => {
549
- const next = new Set(prev)
550
- if (next.has(label)) next.delete(label)
551
- else next.add(label)
552
- return next
553
- })
554
-
555
599
  // ---- role CRUD -----------------------------------------------------------
556
600
  const refreshRoles = async (selectId?: string | null) => {
557
601
  const rs = await loadRoles()
@@ -631,6 +675,11 @@ export function PermissionsManager({
631
675
  const moduleGranted = activeModule && draft ? grantedCountForModule(draft, activeModule) : 0
632
676
  const moduleTotal = activeModule?.actions.length ?? 0
633
677
  const checksDisabled = !activeRole || !draft || loadingPerms || saving
678
+ const activeModuleGroupTitle = React.useMemo(() => {
679
+ if (!activeModule || !groups) return ''
680
+ const g = groups.find((grp) => grp.modules.some((m) => m.key === activeModule.key))
681
+ return g?.title ?? ''
682
+ }, [activeModule, groups])
634
683
 
635
684
  // ---- render --------------------------------------------------------------
636
685
  if (loadError) {
@@ -809,13 +858,13 @@ export function PermissionsManager({
809
858
  )}
810
859
  </div>
811
860
 
812
- {(catalog?.general.length ?? 0) > 0 && (
861
+ {(general?.length ?? 0) > 0 && (
813
862
  <>
814
863
  <Separator />
815
864
  <div>
816
865
  <h3 className="mb-2 text-sm font-semibold">Permisos Generales</h3>
817
866
  <div className="flex flex-col gap-2">
818
- {catalog!.general.map((g) => (
867
+ {general!.map((g) => (
819
868
  <CapabilityCheck
820
869
  key={g.key}
821
870
  checked={draft?.has(g.key) ?? false}
@@ -832,97 +881,104 @@ export function PermissionsManager({
832
881
  </CardContent>
833
882
  </Card>
834
883
 
835
- {/* Card: Módulos (hierarchical tree, mirrors the sidebar) */}
884
+ {/* Card: Módulo a grouped combobox, same pattern as the role
885
+ selector above (compact; the long flat list felt heavy). */}
836
886
  <Card>
837
887
  <CardHeader>
838
- <CardTitle className="text-base">Módulos</CardTitle>
888
+ <CardTitle className="text-base">Módulo</CardTitle>
839
889
  <CardDescription>
840
890
  Elige el módulo cuyas acciones quieres configurar.
841
891
  </CardDescription>
842
892
  </CardHeader>
843
- <CardContent className="flex flex-col gap-3">
844
- <div className="relative">
845
- <Search className="pointer-events-none absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
846
- <Input
847
- value={moduleQuery}
848
- onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
849
- setModuleQuery(e.target.value)
850
- }
851
- placeholder="Buscar módulo…"
852
- aria-label="Buscar módulo"
853
- className="pl-8"
854
- />
855
- </div>
856
-
857
- <div
858
- role="tree"
859
- aria-label="Módulos"
860
- className="-mx-1 max-h-[460px] overflow-y-auto px-1"
861
- >
862
- {visibleGroups.length === 0 ? (
863
- <p className="px-2 py-6 text-center text-sm text-muted-foreground">
864
- Sin módulos.
865
- </p>
866
- ) : (
867
- visibleGroups.map((group) => {
868
- // While searching, force every matching group open.
869
- const open = searching || !collapsedGroups.has(group.label)
870
- return (
871
- <Collapsible
872
- key={group.label}
873
- open={open}
874
- onOpenChange={() =>
875
- !searching && toggleGroup(group.label)
876
- }
877
- >
878
- <CollapsibleTrigger asChild>
879
- <button
880
- type="button"
881
- className="flex w-full items-center gap-1.5 rounded-md px-2 py-1.5 text-left text-xs font-semibold uppercase tracking-wide text-muted-foreground transition-colors hover:bg-muted/40"
882
- >
883
- <ChevronRight
884
- className={cn(
885
- 'h-3.5 w-3.5 shrink-0 transition-transform',
886
- open && 'rotate-90',
887
- )}
888
- />
889
- <Folder className="h-3.5 w-3.5 shrink-0" />
890
- <span className="min-w-0 flex-1 truncate normal-case">
891
- {group.label}
892
- </span>
893
- <span className="shrink-0 text-[10px] tabular-nums opacity-70">
894
- {group.modules.length}
895
- </span>
896
- </button>
897
- </CollapsibleTrigger>
898
- <CollapsibleContent>
899
- <div className="ml-3 flex flex-col gap-0.5 border-l border-border/60 pl-1.5">
900
- {group.modules.map((mod) => (
901
- <ModuleTreeItem
902
- key={mod.key}
903
- module={mod}
904
- active={mod.key === activeModuleKey}
905
- granted={
906
- draft
907
- ? grantedCountForModule(
908
- draft,
909
- mod,
910
- )
911
- : 0
912
- }
913
- total={mod.actions.length}
914
- onSelect={() =>
915
- setActiveModuleKey(mod.key)
893
+ <CardContent>
894
+ <Popover open={moduleOpen} onOpenChange={setModuleOpen}>
895
+ <PopoverTrigger asChild>
896
+ <Button
897
+ variant="outline"
898
+ role="combobox"
899
+ aria-expanded={moduleOpen}
900
+ className="w-full justify-between font-normal"
901
+ >
902
+ <span className="flex min-w-0 items-center gap-2">
903
+ {activeModule && (
904
+ <DynamicIcon
905
+ name={
906
+ activeModule.icon ||
907
+ (activeModule.kind === 'screen'
908
+ ? 'Eye'
909
+ : 'Square')
910
+ }
911
+ className="h-4 w-4 shrink-0 opacity-70"
912
+ />
913
+ )}
914
+ <span className="truncate">
915
+ {activeModule
916
+ ? activeModule.label
917
+ : 'Seleccionar módulo…'}
918
+ </span>
919
+ </span>
920
+ <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
921
+ </Button>
922
+ </PopoverTrigger>
923
+ <PopoverContent
924
+ className="w-[var(--radix-popover-trigger-width)] min-w-[280px] p-0"
925
+ align="start"
926
+ >
927
+ <Command>
928
+ <CommandInput placeholder="Buscar módulo…" />
929
+ <CommandList className="max-h-[360px]">
930
+ <CommandEmpty>Sin módulos.</CommandEmpty>
931
+ {(groups ?? []).map((group, gi) => (
932
+ <CommandGroup
933
+ key={group.title || `__untitled_${gi}`}
934
+ heading={group.title || undefined}
935
+ >
936
+ {group.modules.map((mod) => (
937
+ <CommandItem
938
+ key={mod.key}
939
+ value={`${group.title} ${mod.label} ${mod.key}`}
940
+ onSelect={() => {
941
+ setActiveModuleKey(mod.key)
942
+ setModuleOpen(false)
943
+ }}
944
+ >
945
+ <DynamicIcon
946
+ name={
947
+ mod.icon ||
948
+ (mod.kind === 'screen'
949
+ ? 'Eye'
950
+ : 'Square')
916
951
  }
952
+ className="mr-2 h-4 w-4 shrink-0 opacity-70"
917
953
  />
918
- ))}
919
- </div>
920
- </CollapsibleContent>
921
- </Collapsible>
922
- )
923
- })
924
- )}
925
- </div>
954
+ <span className="truncate">
955
+ {mod.label}
956
+ </span>
957
+ {draft &&
958
+ grantedCountForModule(draft, mod) >
959
+ 0 && (
960
+ <Badge
961
+ variant="secondary"
962
+ className="ml-auto shrink-0 tabular-nums"
963
+ >
964
+ {grantedCountForModule(
965
+ draft,
966
+ mod,
967
+ )}
968
+ /{mod.actions.length}
969
+ </Badge>
970
+ )}
971
+ {mod.key === activeModuleKey && (
972
+ <Check className="ml-2 h-4 w-4 shrink-0" />
973
+ )}
974
+ </CommandItem>
975
+ ))}
976
+ </CommandGroup>
977
+ ))}
978
+ </CommandList>
979
+ </Command>
980
+ </PopoverContent>
981
+ </Popover>
926
982
  </CardContent>
927
983
  </Card>
928
984
  </div>
@@ -935,7 +991,10 @@ export function PermissionsManager({
935
991
  <CardTitle className="flex items-center gap-2 text-base">
936
992
  {activeModule && (
937
993
  <DynamicIcon
938
- name={activeModule.icon || 'Square'}
994
+ name={
995
+ activeModule.icon ||
996
+ (activeModule.kind === 'screen' ? 'Eye' : 'Square')
997
+ }
939
998
  className="h-4 w-4 shrink-0 text-primary"
940
999
  />
941
1000
  )}
@@ -945,7 +1004,9 @@ export function PermissionsManager({
945
1004
  </CardTitle>
946
1005
  <CardDescription>
947
1006
  {activeModule
948
- ? `${moduleGroupLabel(activeModule)} · configura las acciones permitidas`
1007
+ ? `${
1008
+ activeModuleGroupTitle || 'Sistema'
1009
+ } · configura las acciones permitidas`
949
1010
  : 'Configura los permisos del módulo seleccionado.'}
950
1011
  </CardDescription>
951
1012
  </div>
@@ -986,7 +1047,7 @@ export function PermissionsManager({
986
1047
  ))}
987
1048
  </div>
988
1049
  ) : !activeModule ? (
989
- <EmptyHint text="Selecciona un módulo del árbol para ver sus acciones." />
1050
+ <EmptyHint text="Selecciona un módulo de la lista para ver sus acciones." />
990
1051
  ) : (
991
1052
  <div className="grid gap-2 sm:grid-cols-2 xl:grid-cols-3">
992
1053
  {activeModule.actions.map((action) => {