@flamingo-stack/openframe-frontend-core 0.0.442-snapshot.20260717134227 → 0.0.442

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.
Files changed (107) hide show
  1. package/dist/{chunk-MG4AMIFX.js → chunk-2BTDPDME.js} +1 -4
  2. package/dist/chunk-2BTDPDME.js.map +1 -0
  3. package/dist/{chunk-3ZE55IH6.js → chunk-63TSTDME.js} +5 -5
  4. package/dist/{chunk-FQASXKBK.js → chunk-7KDR5P23.js} +2 -2
  5. package/dist/{chunk-RQVR7I73.js → chunk-7KNOHGMI.js} +2 -2
  6. package/dist/{chunk-RYKANQE6.cjs → chunk-7QP65IE4.cjs} +5 -5
  7. package/dist/{chunk-RYKANQE6.cjs.map → chunk-7QP65IE4.cjs.map} +1 -1
  8. package/dist/{chunk-LGMRNYTX.js → chunk-AGV26PUR.js} +3 -3
  9. package/dist/{chunk-IBIGXER5.cjs → chunk-DUQQEQ7G.cjs} +153 -191
  10. package/dist/chunk-DUQQEQ7G.cjs.map +1 -0
  11. package/dist/{chunk-A7MG4SBL.js → chunk-FPE3SQ5D.js} +3 -3
  12. package/dist/{chunk-6IPNXMB7.js → chunk-IAZ5AN2E.js} +6 -6
  13. package/dist/{chunk-ZVAAIP5Y.cjs → chunk-IPEIOR77.cjs} +40 -40
  14. package/dist/{chunk-ZVAAIP5Y.cjs.map → chunk-IPEIOR77.cjs.map} +1 -1
  15. package/dist/{chunk-KNNSS3CZ.cjs → chunk-IZ6A6K6N.cjs} +15 -15
  16. package/dist/{chunk-KNNSS3CZ.cjs.map → chunk-IZ6A6K6N.cjs.map} +1 -1
  17. package/dist/{chunk-2CJ734N2.cjs → chunk-J5NO7PAG.cjs} +3 -3
  18. package/dist/{chunk-2CJ734N2.cjs.map → chunk-J5NO7PAG.cjs.map} +1 -1
  19. package/dist/{chunk-VACXEUGF.cjs → chunk-JNC4LVD6.cjs} +29 -29
  20. package/dist/{chunk-VACXEUGF.cjs.map → chunk-JNC4LVD6.cjs.map} +1 -1
  21. package/dist/{chunk-PRJFSF4W.cjs → chunk-JOH5D76X.cjs} +73 -73
  22. package/dist/{chunk-PRJFSF4W.cjs.map → chunk-JOH5D76X.cjs.map} +1 -1
  23. package/dist/{chunk-DOVBNECK.js → chunk-JYI4R25Q.js} +3 -3
  24. package/dist/{chunk-OHKKI6EF.cjs → chunk-KUXGQ2FQ.cjs} +32 -32
  25. package/dist/{chunk-OHKKI6EF.cjs.map → chunk-KUXGQ2FQ.cjs.map} +1 -1
  26. package/dist/{chunk-H4W3NCR2.cjs → chunk-LFWPWIAC.cjs} +7 -7
  27. package/dist/{chunk-H4W3NCR2.cjs.map → chunk-LFWPWIAC.cjs.map} +1 -1
  28. package/dist/{chunk-YPFQ2UIH.cjs → chunk-LUKRPHEW.cjs} +73 -73
  29. package/dist/{chunk-YPFQ2UIH.cjs.map → chunk-LUKRPHEW.cjs.map} +1 -1
  30. package/dist/{chunk-UXJ47OJF.js → chunk-QAG357PE.js} +2 -2
  31. package/dist/{chunk-7CZ4MJSI.cjs → chunk-QO6MCQYE.cjs} +10 -13
  32. package/dist/chunk-QO6MCQYE.cjs.map +1 -0
  33. package/dist/{chunk-AZQOX2RS.js → chunk-SM26C2L7.js} +2 -2
  34. package/dist/{chunk-M6XUXQ5Q.cjs → chunk-TAQMYVO6.cjs} +40 -40
  35. package/dist/{chunk-M6XUXQ5Q.cjs.map → chunk-TAQMYVO6.cjs.map} +1 -1
  36. package/dist/{chunk-323TCNF3.cjs → chunk-UFBUQO7Z.cjs} +18 -18
  37. package/dist/{chunk-323TCNF3.cjs.map → chunk-UFBUQO7Z.cjs.map} +1 -1
  38. package/dist/{chunk-PVMPBWKX.js → chunk-W3JZXLJ2.js} +6 -6
  39. package/dist/{chunk-6NO4YYU7.cjs → chunk-XMCVUYKC.cjs} +12 -12
  40. package/dist/{chunk-6NO4YYU7.cjs.map → chunk-XMCVUYKC.cjs.map} +1 -1
  41. package/dist/{chunk-5U6OZ5FD.js → chunk-Y3NFB6HR.js} +4 -4
  42. package/dist/{chunk-LYYVTI34.js → chunk-YZJUHEPZ.js} +5 -5
  43. package/dist/{chunk-NQODKJ2F.js → chunk-ZQYFDYWV.js} +83 -121
  44. package/dist/{chunk-NQODKJ2F.js.map → chunk-ZQYFDYWV.js.map} +1 -1
  45. package/dist/components/case-studies/index.cjs +9 -9
  46. package/dist/components/case-studies/index.js +3 -3
  47. package/dist/components/chat/index.cjs +3 -3
  48. package/dist/components/chat/index.js +2 -2
  49. package/dist/components/contact/index.cjs +4 -4
  50. package/dist/components/contact/index.js +3 -3
  51. package/dist/components/docs/index.cjs +6 -6
  52. package/dist/components/docs/index.js +5 -5
  53. package/dist/components/embeds/index.cjs +4 -4
  54. package/dist/components/embeds/index.js +3 -3
  55. package/dist/components/faq/index.cjs +5 -5
  56. package/dist/components/faq/index.js +4 -4
  57. package/dist/components/features/index.cjs +3 -3
  58. package/dist/components/features/index.js +2 -2
  59. package/dist/components/features/policy-configuration-panel.d.ts +0 -11
  60. package/dist/components/features/policy-configuration-panel.d.ts.map +1 -1
  61. package/dist/components/help-center-pages/index.cjs +22 -22
  62. package/dist/components/help-center-pages/index.js +13 -13
  63. package/dist/components/index.cjs +135 -135
  64. package/dist/components/index.js +11 -11
  65. package/dist/components/navigation/index.cjs +3 -3
  66. package/dist/components/navigation/index.js +2 -2
  67. package/dist/components/onboarding-guides/index.cjs +7 -7
  68. package/dist/components/onboarding-guides/index.js +6 -6
  69. package/dist/components/related-content/index.cjs +5 -5
  70. package/dist/components/related-content/index.js +4 -4
  71. package/dist/components/tickets/index.cjs +6 -6
  72. package/dist/components/tickets/index.js +5 -5
  73. package/dist/components/ui/index.cjs +3 -3
  74. package/dist/components/ui/index.js +2 -2
  75. package/dist/hooks/index.cjs +2 -2
  76. package/dist/hooks/index.js +1 -1
  77. package/dist/index.cjs +3 -3
  78. package/dist/index.js +2 -2
  79. package/dist/types/permissions.d.ts +6 -9
  80. package/dist/types/permissions.d.ts.map +1 -1
  81. package/dist/utils/embed-authed-fetch.d.ts +3 -19
  82. package/dist/utils/embed-authed-fetch.d.ts.map +1 -1
  83. package/dist/utils/index.cjs +0 -3
  84. package/dist/utils/index.cjs.map +1 -1
  85. package/dist/utils/index.js +0 -3
  86. package/dist/utils/index.js.map +1 -1
  87. package/package.json +1 -1
  88. package/src/components/features/policy-configuration-panel.tsx +122 -185
  89. package/src/types/permissions.ts +7 -10
  90. package/src/utils/__tests__/embed-authed-fetch.test.ts +0 -39
  91. package/src/utils/embed-authed-fetch.ts +3 -27
  92. package/dist/chunk-7CZ4MJSI.cjs.map +0 -1
  93. package/dist/chunk-IBIGXER5.cjs.map +0 -1
  94. package/dist/chunk-MG4AMIFX.js.map +0 -1
  95. package/src/stories/PolicyConfigurationPanel.stories.tsx +0 -254
  96. /package/dist/{chunk-3ZE55IH6.js.map → chunk-63TSTDME.js.map} +0 -0
  97. /package/dist/{chunk-FQASXKBK.js.map → chunk-7KDR5P23.js.map} +0 -0
  98. /package/dist/{chunk-RQVR7I73.js.map → chunk-7KNOHGMI.js.map} +0 -0
  99. /package/dist/{chunk-LGMRNYTX.js.map → chunk-AGV26PUR.js.map} +0 -0
  100. /package/dist/{chunk-A7MG4SBL.js.map → chunk-FPE3SQ5D.js.map} +0 -0
  101. /package/dist/{chunk-6IPNXMB7.js.map → chunk-IAZ5AN2E.js.map} +0 -0
  102. /package/dist/{chunk-DOVBNECK.js.map → chunk-JYI4R25Q.js.map} +0 -0
  103. /package/dist/{chunk-UXJ47OJF.js.map → chunk-QAG357PE.js.map} +0 -0
  104. /package/dist/{chunk-AZQOX2RS.js.map → chunk-SM26C2L7.js.map} +0 -0
  105. /package/dist/{chunk-PVMPBWKX.js.map → chunk-W3JZXLJ2.js.map} +0 -0
  106. /package/dist/{chunk-5U6OZ5FD.js.map → chunk-Y3NFB6HR.js.map} +0 -0
  107. /package/dist/{chunk-LYYVTI34.js.map → chunk-YZJUHEPZ.js.map} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flamingo-stack/openframe-frontend-core",
3
- "version": "0.0.442-snapshot.20260717134227",
3
+ "version": "0.0.442",
4
4
  "description": "Shared design system and components for all Flamingo platforms",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -2,7 +2,7 @@
2
2
 
3
3
  import * as React from 'react'
4
4
  import { useRef, useEffect, useState } from 'react'
5
- import { ChevronDown, Shield } from 'lucide-react'
5
+ import { ChevronDown } from 'lucide-react'
6
6
  import { cn } from '../../utils/cn'
7
7
  import { ToolIcon } from '../tool-icon'
8
8
  import { Button } from '../ui/button'
@@ -34,110 +34,62 @@ const getApprovalLevelLabel = (level: ApprovalLevel | undefined, editMode: boole
34
34
  return option?.label || level
35
35
  }
36
36
 
37
- const ApprovalLevelDropdown: React.FC<{
38
- value: ApprovalLevel | undefined;
39
- onChange: (level: ApprovalLevel | undefined) => void;
40
- /** Adds the "Clear Global Permission" item (category bulk dropdown). */
41
- allowClear?: boolean;
42
- triggerClassName?: string;
43
- contentClassName?: string;
44
- }> = ({ value, onChange, allowClear = false, triggerClassName, contentClassName }) => (
45
- <DropdownMenu>
46
- <DropdownMenuTrigger asChild>
47
- <Button
48
- variant="outline"
49
- rightIcon={<ChevronDown className="h-6 w-6 text-ods-text-secondary" />}
50
- className={cn(
51
- "!text-h6 bg-ods-card border border-ods-border rounded-md flex gap-[var(--spacing-system-xsf)] items-center justify-between !px-[var(--spacing-system-xsf)] py-[var(--spacing-system-xsf)] hover:bg-ods-bg-hover transition-colors h-auto",
52
- value ? "text-ods-text-primary" : "text-ods-text-secondary",
53
- triggerClassName
54
- )}
55
- >
56
- {getApprovalLevelLabel(value, true)}
57
- </Button>
58
- </DropdownMenuTrigger>
59
- <DropdownMenuContent align="end" className={contentClassName}>
60
- {allowClear && (
61
- <DropdownMenuItem onClick={() => onChange(undefined)} className="text-h6">
62
- Clear Global Permission
63
- </DropdownMenuItem>
64
- )}
65
- {approvalLevelOptions.map(option => (
66
- <DropdownMenuItem
67
- key={option.value}
68
- onClick={() => onChange(option.value)}
69
- className="text-h6"
70
- >
71
- {option.label}
72
- </DropdownMenuItem>
73
- ))}
74
- </DropdownMenuContent>
75
- </DropdownMenu>
76
- )
77
-
78
37
  const PolicyRow: React.FC<{
79
38
  policy: PermissionPolicy;
80
39
  categoryId: string;
81
40
  editMode: boolean;
82
41
  onPermissionChange: (categoryId: string, policyId: string, level: ApprovalLevel) => void;
83
42
  }> = ({ policy, categoryId, editMode, onPermissionChange }) => {
84
- const handleChange = (level: ApprovalLevel | undefined) => {
85
- if (level) onPermissionChange(categoryId, policy.id, level)
86
- }
87
-
88
43
  return (
89
- <div className="bg-ods-bg border-b last:border-b-0 border-ods-border flex gap-[var(--spacing-system-m)] items-start md:items-center px-[var(--spacing-system-m)] py-[var(--spacing-system-sf)]">
44
+ <div className="bg-ods-bg border-b border-ods-border flex gap-4 items-center px-4 py-3">
90
45
  {/* Tool Icon */}
91
- <div className="bg-ods-bg border border-ods-border rounded-md flex items-center justify-center w-8 h-8 shrink-0">
46
+ <div className="bg-ods-bg border border-ods-border rounded-md flex items-center justify-center w-8 h-8">
92
47
  <ToolIcon toolType={policy.toolName} size={16} />
93
48
  </div>
94
49
 
95
- {/* Policy Info — on mobile the approval level stacks below the pattern */}
50
+ {/* Policy Info */}
96
51
  <div className="flex-1 flex flex-col min-w-0">
97
- <p className="text-h4 text-ods-text-primary truncate" title={policy.name}>
52
+ <p className="text-h6 text-ods-text-primary truncate" title={policy.name}>
98
53
  {policy.name}
99
54
  </p>
100
- <p
101
- className="text-h6 text-ods-text-secondary truncate md:whitespace-normal md:break-all"
102
- title={policy.commandPattern}
103
- >
55
+ <p className="text-code text-ods-text-secondary break-all">
104
56
  {policy.commandPattern}
105
57
  </p>
106
- {editMode ? (
107
- <div className="md:hidden mt-[var(--spacing-system-xsf)]">
108
- <ApprovalLevelDropdown
109
- value={policy.approvalLevel}
110
- onChange={handleChange}
111
- triggerClassName="w-full"
112
- contentClassName="w-[180px]"
113
- />
114
- </div>
115
- ) : policy.approvalLevel ? (
116
- <span className="md:hidden text-h4 text-ods-text-primary">
117
- {getApprovalLevelLabel(policy.approvalLevel)}
118
- </span>
119
- ) : null}
120
58
  </div>
121
59
 
122
- {/* Approval Level column (desktop only) */}
123
- <div className="hidden md:block shrink-0">
124
- {editMode ? (
125
- <ApprovalLevelDropdown
126
- value={policy.approvalLevel}
127
- onChange={handleChange}
128
- triggerClassName="w-[180px]"
129
- contentClassName="w-[180px]"
130
- />
131
- ) : (
132
- <div className="px-[var(--spacing-system-sf)] py-[var(--spacing-system-xsf)] w-[180px]">
133
- {policy.approvalLevel && (
134
- <span className="text-h4 text-ods-text-primary">
135
- {getApprovalLevelLabel(policy.approvalLevel)}
136
- </span>
137
- )}
138
- </div>
139
- )}
140
- </div>
60
+ {/* Approval Level Dropdown */}
61
+ {editMode ? (
62
+ <DropdownMenu>
63
+ <DropdownMenuTrigger asChild>
64
+ <Button
65
+ variant="outline"
66
+ rightIcon={<ChevronDown className="h-6 w-6 text-ods-text-secondary" />}
67
+ className="md:!text-sm bg-ods-card border border-ods-border rounded-md flex gap-2 items-center justify-between !px-2 py-2 w-[180px] hover:bg-ods-bg-hover transition-colors text-ods-text-primary h-auto"
68
+ >
69
+ {getApprovalLevelLabel(policy.approvalLevel, editMode)}
70
+ </Button>
71
+ </DropdownMenuTrigger>
72
+ <DropdownMenuContent align="end" className="w-[180px]">
73
+ {approvalLevelOptions.map(option => (
74
+ <DropdownMenuItem
75
+ key={option.value}
76
+ onClick={() => onPermissionChange(categoryId, policy.id, option.value)}
77
+ className="text-h6"
78
+ >
79
+ {option.label}
80
+ </DropdownMenuItem>
81
+ ))}
82
+ </DropdownMenuContent>
83
+ </DropdownMenu>
84
+ ) : policy.approvalLevel ? (
85
+ <div className="px-3 py-2 w-[180px]">
86
+ <span className="text-h6 text-ods-text-primary">
87
+ {getApprovalLevelLabel(policy.approvalLevel, editMode)}
88
+ </span>
89
+ </div>
90
+ ) : (
91
+ <div className="px-3 py-2 w-[180px]" />
92
+ )}
141
93
  </div>
142
94
  )
143
95
  }
@@ -145,7 +97,7 @@ const PolicyRow: React.FC<{
145
97
  const useAnimatedHeight = (isExpanded: boolean) => {
146
98
  const [height, setHeight] = useState<number | 'auto'>(0);
147
99
  const contentRef = useRef<HTMLDivElement>(null);
148
-
100
+
149
101
  useEffect(() => {
150
102
  if (contentRef.current) {
151
103
  const contentHeight = contentRef.current.scrollHeight;
@@ -159,73 +111,93 @@ const useAnimatedHeight = (isExpanded: boolean) => {
159
111
  const CategorySection: React.FC<{
160
112
  category: PermissionCategory;
161
113
  editMode: boolean;
162
- isExpanded: boolean;
163
- /** Last bulk level applied to this category (display-only, edit mode). */
164
- bulkLevel: ApprovalLevel | undefined;
165
- onToggle: (categoryId: string) => void;
166
- onBulkLevelChange: (categoryId: string, level: ApprovalLevel | undefined) => void;
114
+ onCategoryToggle: (categoryId: string) => void;
115
+ onGlobalPermissionChange: (categoryId: string, level: ApprovalLevel | undefined) => void;
167
116
  onPolicyPermissionChange: (categoryId: string, policyId: string, level: ApprovalLevel) => void;
168
- }> = ({
169
- category,
170
- editMode,
171
- isExpanded,
172
- bulkLevel,
173
- onToggle,
174
- onBulkLevelChange,
175
- onPolicyPermissionChange
117
+ }> = ({
118
+ category,
119
+ editMode,
120
+ onCategoryToggle,
121
+ onGlobalPermissionChange,
122
+ onPolicyPermissionChange
176
123
  }) => {
177
- const { contentRef, height } = useAnimatedHeight(isExpanded);
178
- const handleBulkChange = (level: ApprovalLevel | undefined) => onBulkLevelChange(category.id, level)
179
-
124
+ const { contentRef, height } = useAnimatedHeight(category.isExpanded);
125
+
180
126
  return (
181
127
  <>
182
128
  {/* Category Header */}
183
- <div
184
- className="bg-ods-card border-t first:border-t-0 border-ods-border flex gap-[var(--spacing-system-m)] items-center pl-[var(--spacing-system-m)] pr-[var(--spacing-system-xsf)] py-[var(--spacing-system-s)] cursor-pointer hover:bg-ods-bg-hover transition-colors"
185
- onClick={() => onToggle(category.id)}
129
+ <div
130
+ className="bg-ods-card border-t border-ods-border flex gap-4 items-center pl-4 pr-2 py-3 cursor-pointer hover:bg-ods-bg-hover transition-colors"
131
+ onClick={() => onCategoryToggle(category.id)}
186
132
  >
187
133
  {/* Category Icon */}
188
- <div className="bg-ods-bg border border-ods-border rounded-md flex items-center justify-center w-8 h-8 shrink-0">
134
+ <div className="bg-ods-bg border border-ods-border rounded-md flex items-center justify-center w-8 h-8">
189
135
  <div className="text-ods-text-secondary">
190
- {category.icon ?? <Shield className="w-4 h-4" />}
136
+ {category.icon}
191
137
  </div>
192
138
  </div>
193
139
 
194
- {/* Category Info — on mobile the bulk dropdown stacks below the count */}
195
- <div className="flex-1 flex flex-col min-w-0">
196
- <p className="text-h4 text-ods-text-primary truncate" title={category.name}>
140
+ {/* Category Info */}
141
+ <div className="flex-1 flex flex-col">
142
+ <p className="md:!text-sm font-medium text-ods-text-primary">
197
143
  {category.name}
198
144
  </p>
199
145
  <p className="text-h6 text-ods-text-secondary">
200
- {category.policies.length} Configurations
146
+ {category.configurationsCount} Configurations
201
147
  </p>
202
- {editMode && (
203
- <div className="md:hidden mt-[var(--spacing-system-xsf)]" onClick={(e) => e.stopPropagation()}>
204
- <ApprovalLevelDropdown
205
- value={bulkLevel}
206
- onChange={handleBulkChange}
207
- allowClear
208
- triggerClassName="w-full"
209
- contentClassName="w-[256px]"
210
- />
211
- </div>
212
- )}
213
148
  </div>
214
149
 
215
- {/* Global Permission column (desktop only) */}
150
+ {/* Global Permission Dropdown */}
216
151
  {editMode ? (
217
- <div className="hidden md:block shrink-0" onClick={(e) => e.stopPropagation()}>
218
- <ApprovalLevelDropdown
219
- value={bulkLevel}
220
- onChange={handleBulkChange}
221
- allowClear
222
- triggerClassName="w-[256px]"
223
- contentClassName="w-[256px]"
224
- />
152
+ <div onClick={(e) => e.stopPropagation()}>
153
+ <DropdownMenu>
154
+ <DropdownMenuTrigger asChild>
155
+ <Button
156
+ variant="outline"
157
+ rightIcon={<ChevronDown className="h-6 w-6 text-ods-text-secondary" />}
158
+ className="md:!text-sm bg-ods-card border border-ods-border rounded-md flex gap-2 items-center justify-between !px-2 py-2 w-[256px] hover:bg-ods-bg-hover transition-colors h-auto"
159
+ >
160
+ <span className={cn(
161
+ "md:!text-sm font-medium",
162
+ category.globalPermission
163
+ ? "text-ods-text-primary"
164
+ : "text-ods-text-secondary"
165
+ )}>
166
+ {getApprovalLevelLabel(category.globalPermission, editMode)}
167
+ </span>
168
+ </Button>
169
+ </DropdownMenuTrigger>
170
+ <DropdownMenuContent align="end" className="w-[256px]">
171
+ <DropdownMenuItem
172
+ onClick={() => onGlobalPermissionChange(category.id, undefined)}
173
+ className="md:!text-sm"
174
+ >
175
+ Clear Global Permission
176
+ </DropdownMenuItem>
177
+ {approvalLevelOptions.map(option => (
178
+ <DropdownMenuItem
179
+ key={option.value}
180
+ onClick={() => onGlobalPermissionChange(category.id, option.value)}
181
+ className="md:!text-sm"
182
+ >
183
+ {option.label}
184
+ </DropdownMenuItem>
185
+ ))}
186
+ </DropdownMenuContent>
187
+ </DropdownMenu>
188
+ </div>
189
+ ) : category.globalPermission ? (
190
+ <div
191
+ className="px-3 py-2 w-[256px]"
192
+ onClick={(e) => e.stopPropagation()}
193
+ >
194
+ <span className="md:!text-sm font-medium text-ods-text-primary">
195
+ {getApprovalLevelLabel(category.globalPermission, editMode)}
196
+ </span>
225
197
  </div>
226
198
  ) : (
227
- <div
228
- className="hidden md:block px-[var(--spacing-system-sf)] py-[var(--spacing-system-xsf)] w-[256px] shrink-0"
199
+ <div
200
+ className="px-3 py-2 w-[256px]"
229
201
  onClick={(e) => e.stopPropagation()}
230
202
  />
231
203
  )}
@@ -236,32 +208,32 @@ const CategorySection: React.FC<{
236
208
  variant="transparent"
237
209
  onClick={(e) => {
238
210
  e.stopPropagation();
239
- onToggle(category.id);
211
+ onCategoryToggle(category.id);
240
212
  }}
241
- leftIcon={<ChevronDown
213
+ leftIcon={<ChevronDown
242
214
  className={cn(
243
215
  "h-6 w-6 text-ods-text-primary transition-transform duration-300",
244
- isExpanded && "rotate-180"
245
- )}
216
+ category.isExpanded && "rotate-180"
217
+ )}
246
218
  />}
247
- className="rounded-md h-auto w-auto shrink-0"
248
- aria-label={isExpanded ? "Collapse" : "Expand"}
219
+ className="rounded-md h-auto w-auto"
220
+ aria-label={category.isExpanded ? "Collapse" : "Expand"}
249
221
  />
250
222
  </div>
251
223
 
252
224
  {/* Policies List */}
253
- <div
225
+ <div
254
226
  ref={contentRef}
255
- style={{
227
+ style={{
256
228
  height: height === 'auto' ? 'auto' : `${height}px`,
257
229
  transition: 'height 300ms ease-in-out, opacity 300ms ease-in-out'
258
230
  }}
259
231
  className={cn(
260
232
  "overflow-hidden",
261
- isExpanded ? "opacity-100" : "opacity-0"
233
+ category.isExpanded ? "opacity-100" : "opacity-0"
262
234
  )}
263
235
  >
264
- <div className="px-[var(--spacing-system-mf)] pb-[var(--spacing-system-mf)] pt-0">
236
+ <div className="px-4 pb-4 pt-0">
265
237
  <div className="border border-ods-border rounded-md overflow-hidden">
266
238
  {category.policies.map((policy) => (
267
239
  <PolicyRow
@@ -279,47 +251,14 @@ const CategorySection: React.FC<{
279
251
  )
280
252
  }
281
253
 
282
- /**
283
- * Grouped list of permission categories with per-policy approval dropdowns.
284
- * Presentation state — which categories are expanded and the last bulk level
285
- * picked per category — lives inside the panel, so callers pass pure data and
286
- * background data refreshes never collapse the user's view. Bulk selections
287
- * reset when `editMode` changes.
288
- *
289
- * Responsive: below `md` the approval level / dropdowns stack inside the text
290
- * column (per the ODS mobile design); at `md+` they render as fixed-width
291
- * right-hand columns.
292
- */
293
254
  export const PolicyConfigurationPanel: React.FC<PolicyConfigurationPanelProps> = ({
294
255
  categories,
295
256
  editMode,
257
+ onCategoryToggle,
258
+ onGlobalPermissionChange,
296
259
  onPolicyPermissionChange,
297
- onCategoryPermissionChange,
298
260
  className
299
261
  }) => {
300
- const [expandedIds, setExpandedIds] = useState<ReadonlySet<string>>(new Set())
301
- const [bulkLevels, setBulkLevels] = useState<Record<string, ApprovalLevel | undefined>>({})
302
-
303
- useEffect(() => {
304
- setBulkLevels({})
305
- }, [editMode])
306
-
307
- const handleToggle = (categoryId: string) => {
308
- setExpandedIds(prev => {
309
- const next = new Set(prev)
310
- if (next.has(categoryId)) next.delete(categoryId)
311
- else next.add(categoryId)
312
- return next
313
- })
314
- }
315
-
316
- const handleBulkLevelChange = (categoryId: string, level: ApprovalLevel | undefined) => {
317
- setBulkLevels(prev => ({ ...prev, [categoryId]: level }))
318
- if (level) {
319
- onCategoryPermissionChange(categoryId, level)
320
- }
321
- }
322
-
323
262
  return (
324
263
  <div className={cn(
325
264
  "bg-ods-card border border-ods-border rounded-md overflow-hidden",
@@ -330,10 +269,8 @@ export const PolicyConfigurationPanel: React.FC<PolicyConfigurationPanelProps> =
330
269
  key={category.id}
331
270
  category={category}
332
271
  editMode={editMode}
333
- isExpanded={expandedIds.has(category.id)}
334
- bulkLevel={bulkLevels[category.id]}
335
- onToggle={handleToggle}
336
- onBulkLevelChange={handleBulkLevelChange}
272
+ onCategoryToggle={onCategoryToggle}
273
+ onGlobalPermissionChange={onGlobalPermissionChange}
337
274
  onPolicyPermissionChange={onPolicyPermissionChange}
338
275
  />
339
276
  ))}
@@ -341,4 +278,4 @@ export const PolicyConfigurationPanel: React.FC<PolicyConfigurationPanelProps> =
341
278
  )
342
279
  }
343
280
 
344
- PolicyConfigurationPanel.displayName = 'PolicyConfigurationPanel'
281
+ PolicyConfigurationPanel.displayName = 'PolicyConfigurationPanel'
@@ -11,24 +11,21 @@ export interface PermissionPolicy {
11
11
  approvalLevel: ApprovalLevel;
12
12
  }
13
13
 
14
- /**
15
- * Pure data describing one permission category. Presentation state (expansion,
16
- * bulk-selection display) is owned by PolicyConfigurationPanel itself.
17
- */
18
14
  export interface PermissionCategory {
19
15
  id: string;
20
16
  name: string;
21
- /** Optional leading icon; the panel falls back to a shield. */
22
- icon?: React.ReactNode;
17
+ icon: React.ReactNode;
18
+ configurationsCount: number;
19
+ globalPermission?: ApprovalLevel;
20
+ isExpanded: boolean;
23
21
  policies: PermissionPolicy[];
24
22
  }
25
23
 
26
24
  export interface PolicyConfigurationPanelProps {
27
25
  categories: PermissionCategory[];
28
26
  editMode: boolean;
29
- /** Change a single policy's approval level. `policyId` is the policy's `id`. */
27
+ onCategoryToggle: (categoryId: string) => void;
28
+ onGlobalPermissionChange: (categoryId: string, level: ApprovalLevel | undefined) => void;
30
29
  onPolicyPermissionChange: (categoryId: string, policyId: string, level: ApprovalLevel) => void;
31
- /** Bulk-apply an approval level to every policy in a category. */
32
- onCategoryPermissionChange: (categoryId: string, level: ApprovalLevel) => void;
33
30
  className?: string;
34
- }
31
+ }
@@ -166,42 +166,3 @@ describe('embedAuthedFetch 401 self-heal (adapter.refresh)', () => {
166
166
  expect(fetchSpy).toHaveBeenCalledTimes(1)
167
167
  })
168
168
  })
169
-
170
- describe('embedAuthedFetch adapter.allowedOrigins (native-shell cross-origin sanction)', () => {
171
- const originalFetch = globalThis.fetch
172
-
173
- beforeEach(() => {
174
- globalThis.fetch = vi.fn(async () => new Response('ok')) as typeof fetch
175
- // Production mode so these tests exercise the real guard, not the
176
- // dev-mode warn-and-allow hatch.
177
- vi.stubEnv('NODE_ENV', 'production')
178
- })
179
- afterEach(() => {
180
- globalThis.fetch = originalFetch
181
- setEmbedAuthAdapter(null)
182
- vi.unstubAllEnvs()
183
- vi.restoreAllMocks()
184
- })
185
-
186
- it('allows a cross-origin URL whose exact origin the adapter sanctions', async () => {
187
- setEmbedAuthAdapter({ allowedOrigins: ['https://gateway.example'] })
188
- await expect(embedAuthedFetch('https://gateway.example/content/api/faqs')).resolves.toBeDefined()
189
- })
190
-
191
- it('still rejects origins outside the allowlist', () => {
192
- setEmbedAuthAdapter({ allowedOrigins: ['https://gateway.example'] })
193
- expect(() => embedAuthedFetch('https://evil.com/x')).toThrow(/cross-origin/i)
194
- })
195
-
196
- it('does not treat an allowlisted origin as a prefix (suffix-domain spoof rejected)', () => {
197
- setEmbedAuthAdapter({ allowedOrigins: ['https://gateway.example'] })
198
- expect(() => embedAuthedFetch('https://gateway.example.evil.com/x')).toThrow(/cross-origin/i)
199
- })
200
-
201
- it('cannot allowlist non-http(s) schemes (protocol check runs first)', () => {
202
- // `javascript:` URLs have origin 'null'; even a (misguided) 'null' entry
203
- // must not get past the protocol gate.
204
- setEmbedAuthAdapter({ allowedOrigins: ['null'] })
205
- expect(() => embedAuthedFetch('javascript:alert(1)')).toThrow(/non-http\(s\)/i)
206
- })
207
- })
@@ -86,19 +86,6 @@ export interface EmbedAuthAdapter {
86
86
  * host's own auth layer, not to this fetch wrapper.
87
87
  */
88
88
  refresh?: () => Promise<boolean>
89
- /**
90
- * Exact extra origins (`scheme://host[:port]`, as produced by `URL.origin`)
91
- * the same-origin guard additionally accepts. The guard normally rejects
92
- * every cross-origin URL in production — but native-shell hosts serve the
93
- * page from a local pseudo-origin (`capacitor://localhost`,
94
- * `tauri://localhost`) with NO server behind it, so the gateway their
95
- * bearer already belongs to is unavoidably cross-origin. Listing it here
96
- * is the host's explicit sanction to send the adapter's credentials there.
97
- * Compared AFTER the http(s)-protocol check, so non-http(s) schemes can
98
- * never be allowlisted. Omit (or leave empty) everywhere else — same-origin
99
- * remains the rule, this is the narrow exception.
100
- */
101
- allowedOrigins?: string[]
102
89
  }
103
90
 
104
91
  /**
@@ -171,12 +158,9 @@ export function hasEmbedAuthAdapter(): boolean {
171
158
  *
172
159
  * **Cross-origin defense:** the wrapper assumes a same-origin `/api/…`
173
160
  * relative URL. Absolute URLs are accepted only when their origin matches
174
- * the current window's origin — or appears in the registered adapter's
175
- * `allowedOrigins` (the native-shell hatch; see that field's doc) — and
176
- * cross-origin URLs otherwise throw before the bearer leaves the page.
177
- * This is a defense-in-depth guard for future call sites — outside the
178
- * allowlisted-shell case there is no legitimate cross-origin use of this
179
- * fetch wrapper.
161
+ * the current window's origin; cross-origin URLs throw before the bearer
162
+ * leaves the page. This is a defense-in-depth guard for future call sites
163
+ * — there is no legitimate cross-origin use of this fetch wrapper.
180
164
  *
181
165
  * **401 self-heal:** when a registered adapter supplies `refresh`, a `401`
182
166
  * response triggers a single token refresh + retry of the same request
@@ -368,14 +352,6 @@ function assertSameOrigin(url: string): void {
368
352
  )
369
353
  }
370
354
  if (target.origin !== pageOrigin) {
371
- // Host-sanctioned cross-origin target (`EmbedAuthAdapter.allowedOrigins`).
372
- // Native shells serve the page from a local pseudo-origin with no server
373
- // behind it, so their gateway — the same backend the host already trusts
374
- // with this bearer — is unavoidably cross-origin. Exact `URL.origin`
375
- // match, and only reachable for http(s) targets (checked above).
376
- if (getRegisteredAuthAdapter()?.allowedOrigins?.includes(target.origin)) {
377
- return
378
- }
379
355
  // Dev-mode escape hatch — embedded apps (e.g. openframe-frontend)
380
356
  // run on a different origin from their gateway during local dev,
381
357
  // and forcing a Next.js `rewrites()` workaround is more error-prone