@startinblox/components-ds4go 3.3.7 → 4.0.0

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.
@@ -930,7 +930,7 @@ export class PolicyComposer extends LitElement {
930
930
  </fieldset>
931
931
  <div class="flex align-right spacer-top">
932
932
  <tems-button type="outline-gray" size="sm" @click=${this._resetPolicy}>
933
- ${msg(`Reset to ${this.template.name} default`)}
933
+ ${msg(str`Reset to ${this.template.name} default`)}
934
934
  </tems-button>
935
935
  </div>
936
936
  </form>`;
@@ -1,18 +1,3 @@
1
- /**
2
- * ODRL Policy Viewer Component
3
- *
4
- * A Lit web component for displaying ODRL policies in a user-friendly format.
5
- *
6
- * Usage:
7
- * ```html
8
- * <odrl-policy-viewer .policy=${policyObject}></odrl-policy-viewer>
9
- * ```
10
- *
11
- * Attributes:
12
- * - policy: OdrlPolicy object to display
13
- * - compact: boolean (optional) - Show compact view
14
- */
15
-
16
1
  import {
17
2
  type OdrlPolicy,
18
3
  OdrlPolicyRenderer,
@@ -171,9 +156,3 @@ export class OdrlPolicyViewer extends LitElement {
171
156
  }
172
157
  }
173
158
  }
174
-
175
- declare global {
176
- interface HTMLElementTagNameMap {
177
- "odrl-policy-viewer": OdrlPolicyViewer;
178
- }
179
- }
@@ -1,5 +1,4 @@
1
1
  import * as utils from "@helpers";
2
- import { msg, str } from "@lit/localize";
3
2
  import { Task } from "@lit/task";
4
3
  import type {
5
4
  Resource,
@@ -12,13 +11,6 @@ import {
12
11
  import { css, html, nothing } from "lit";
13
12
  import { customElement, property, state } from "lit/decorators.js";
14
13
 
15
- export interface DSPProviderConfig {
16
- name: string;
17
- address: string;
18
- color?: string;
19
- participantId?: string;
20
- }
21
-
22
14
  @customElement("solid-dsp-catalog")
23
15
  export class DSPCatalog extends OrbitDSPComponent {
24
16
  constructor() {
@@ -175,40 +167,7 @@ export class DSPCatalog extends OrbitDSPComponent {
175
167
  return html`<tems-viewport>
176
168
  <tems-header slot="header" heading=${this.header}></tems-header>
177
169
  <div slot="content">
178
- ${this.providers.length > 0
179
- ? html`<div
180
- style="background: white; padding: 1rem; border-radius: 8px; margin-bottom: 1rem;"
181
- >
182
- <h3 style="margin: 0 0 1rem 0;">
183
- ${msg("Provider Statistics")}
184
- </h3>
185
- <div
186
- style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem;"
187
- >
188
- ${this.providers.map((provider: DSPProviderConfig) => {
189
- const providerDatasets = datas.filter(
190
- (d: Resource) => d._provider === provider.name,
191
- );
192
- return html`
193
- <div
194
- style="background: #f5f5f5; padding: 1rem; border-radius: 4px; border-left: 4px solid ${provider.color ||
195
- "#1976d2"};"
196
- >
197
- <div
198
- style="font-weight: bold; margin-bottom: 0.5rem;"
199
- >
200
- ${provider.name}
201
- </div>
202
- <div style="font-size: 1.125rem; color: #1976d2;">
203
- ${msg(str`${providerDatasets.length} datasets`)}
204
- </div>
205
- </div>
206
- `;
207
- })}
208
- </div>
209
- </div>`
210
- : nothing}
211
- <tems-catalog-filter-holder
170
+ <ds4go-catalog-filter-holder
212
171
  .displayFiltering=${false}
213
172
  @search=${this._search}
214
173
  .search=${this.search}
@@ -216,7 +175,7 @@ export class DSPCatalog extends OrbitDSPComponent {
216
175
  .objects=${datas}
217
176
  .resultCount=${this.resultCount}
218
177
  .filterCount=${this.filterCount}
219
- ></tems-catalog-filter-holder>
178
+ ></ds4go-catalog-filter-holder>
220
179
  <ds4go-catalog-data-holder
221
180
  .view=${"card"}
222
181
  .objects=${datas}
package/src/ds4go.d.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  import type { LimitedResource } from "@src/component";
2
2
 
3
3
  export interface FactBundle extends LimitedResource {
4
- "@type"?: "ds4go:FactBundle" | "ldp:Container" | ["ldp:Container", "ds4go:FactBundle"];
4
+ "@type"?:
5
+ | "ds4go:FactBundle"
6
+ | "ldp:Container"
7
+ | ["ldp:Container", "ds4go:FactBundle"];
5
8
  name?: string;
6
9
  category?: string;
7
10
  description?: string;
@@ -43,3 +46,77 @@ export interface Media extends LimitedResource {
43
46
  file_type?: string;
44
47
  description?: string;
45
48
  }
49
+
50
+ // ODRL Policy types
51
+ export interface ODRLPolicy {
52
+ "@id"?: string;
53
+ "@type"?: string;
54
+ "odrl:permission"?: ODRLPermission | ODRLPermission[];
55
+ "odrl:prohibition"?: ODRLProhibition | ODRLProhibition[];
56
+ "odrl:obligation"?: ODRLDuty | ODRLDuty[];
57
+ target?: string;
58
+ assigner?: string;
59
+ }
60
+
61
+ export interface ODRLPermission {
62
+ "odrl:action"?: ODRLAction | string;
63
+ "odrl:constraint"?: ODRLConstraint | ODRLConstraint[];
64
+ }
65
+
66
+ export interface ODRLProhibition {
67
+ "odrl:action"?: ODRLAction | string;
68
+ }
69
+
70
+ export interface ODRLDuty {
71
+ "odrl:action"?: ODRLAction | string;
72
+ }
73
+
74
+ export interface ODRLAction {
75
+ "@id"?: string;
76
+ }
77
+
78
+ export interface ODRLConstraint {
79
+ "odrl:leftOperand"?: string;
80
+ "odrl:operator"?: string;
81
+ "odrl:rightOperand"?: string;
82
+ }
83
+
84
+ export interface DcatDistribution {
85
+ "@type": string;
86
+ "dct:format": DctFormat;
87
+ "dcat:accessService": DcatAccessService;
88
+ }
89
+
90
+ export interface DSPProviderConfig {
91
+ name: string;
92
+ address: string;
93
+ color?: string;
94
+ participantId?: string;
95
+ }
96
+
97
+ export interface DSPOffer extends LimitedResource {
98
+ "@id"?: string;
99
+ "@type"?: "dcat:Dataset" | "tems:Service";
100
+ "odrl:hasPolicy"?: ODRLPolicy;
101
+ "dcat:distribution"?: DcatDistribution[];
102
+ category?: string;
103
+ "dcat:endpointUrl"?: string;
104
+ name?: string;
105
+ pricingTier?: string;
106
+ description?: string;
107
+ "dct:language"?: string;
108
+ id?: string;
109
+ contenttype?: string;
110
+ bundleSize?: number;
111
+ previewLinks?: string;
112
+ _provider?: DSPProviderConfig;
113
+ }
114
+
115
+ export interface AgreementInfo {
116
+ contractId: string;
117
+ negotiationId: string;
118
+ timestamp: number;
119
+ assetId: string;
120
+ providerId: string;
121
+ providerAddress: string;
122
+ }
@@ -0,0 +1,243 @@
1
+ import { DSPContractStorage } from "@startinblox/solid-tems-shared";
2
+ import type { AgreementInfo, DSPOffer } from "@src/ds4go";
3
+
4
+ /**
5
+ * Get localStorage key for an asset
6
+ * Uses combination of provider ID and dataset ID for uniqueness across providers
7
+ */
8
+ export function getStorageKey(offer: DSPOffer): string {
9
+ const datasetId = offer["@id"] || offer.id;
10
+ const providerId = offer._provider?.participantId || "";
11
+
12
+ if (!datasetId) return "";
13
+
14
+ // Create composite key: provider-assetId
15
+ const key = providerId
16
+ ? `dsp-agreement-${providerId}-${datasetId}`
17
+ : `dsp-agreement-${datasetId}`;
18
+ return key;
19
+ }
20
+
21
+ /**
22
+ * Save agreement info to localStorage
23
+ */
24
+ export function saveAgreementInfo(
25
+ offer: DSPOffer,
26
+ contractId: string,
27
+ negotiationId: string,
28
+ timestamp: number,
29
+ ): void {
30
+ const key = getStorageKey(offer);
31
+ if (!key) return;
32
+
33
+ const agreementInfo: AgreementInfo = {
34
+ contractId,
35
+ negotiationId,
36
+ timestamp,
37
+ assetId: offer["@id"] || offer.id || "",
38
+ providerId: offer._provider?.participantId || "",
39
+ providerAddress: offer._provider?.address || "",
40
+ };
41
+
42
+ localStorage.setItem(key, JSON.stringify(agreementInfo));
43
+ }
44
+
45
+ /**
46
+ * Load agreement info from localStorage
47
+ */
48
+ export function loadAgreementInfo(offer: DSPOffer): AgreementInfo | null {
49
+ const key = getStorageKey(offer);
50
+ if (!key) return null;
51
+
52
+ try {
53
+ const stored = localStorage.getItem(key);
54
+ if (stored) {
55
+ const info = JSON.parse(stored) as AgreementInfo;
56
+ return info;
57
+ }
58
+ } catch (error) {
59
+ console.error("Failed to load agreement info:", error);
60
+ }
61
+ return null;
62
+ }
63
+
64
+ /**
65
+ * Clear agreement info from localStorage
66
+ */
67
+ export function clearAgreementInfo(offer: DSPOffer): void {
68
+ const key = getStorageKey(offer);
69
+ if (key) {
70
+ localStorage.removeItem(key);
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Save initial contract state when negotiation starts
76
+ */
77
+ export function saveInitialContractState(
78
+ offer: DSPOffer,
79
+ negotiationId: string,
80
+ ): void {
81
+ try {
82
+ const providerId = offer._provider?.participantId || "";
83
+ const assetId = offer["@id"] || offer.id;
84
+
85
+ if (!assetId) return;
86
+
87
+ const existingContracts = DSPContractStorage.getByAssetAndProvider(
88
+ assetId,
89
+ providerId,
90
+ );
91
+ const existingContract = existingContracts.find(
92
+ (c) => c.contractId === negotiationId,
93
+ );
94
+
95
+ if (!existingContract) {
96
+ // Extract index endpoint URL from asset (dcat:endpointUrl)
97
+ const indexEndpointUrl = offer["dcat:endpointUrl"];
98
+
99
+ const assetName = offer.name || assetId || "Unknown Asset";
100
+
101
+ // Detect if this is an index asset
102
+ const isIndexAsset = assetName.toLowerCase().includes("index");
103
+
104
+ // Create new contract in REQUESTED state
105
+ DSPContractStorage.create({
106
+ assetId,
107
+ datasetId: offer["@id"] || offer.id || "",
108
+ assetName,
109
+ assetDescription: offer.description,
110
+ providerName: offer._provider?.name || "Unknown Provider",
111
+ providerAddress: offer._provider?.address || "",
112
+ providerParticipantId: offer._provider?.participantId || "",
113
+ providerColor: offer._provider?.color,
114
+ policy: offer["odrl:hasPolicy"],
115
+ state: "REQUESTED",
116
+ contractId: negotiationId,
117
+ // Index-specific fields
118
+ isIndexAsset,
119
+ indexEndpointUrl,
120
+ });
121
+ }
122
+ } catch (error) {
123
+ console.error(
124
+ "[DSP Contract Catalog] Failed to save initial contract state:",
125
+ error,
126
+ );
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Save contract to DSP Contract Catalog for history tracking
132
+ */
133
+ export function saveToContractCatalog(
134
+ offer: DSPOffer,
135
+ contractId: string,
136
+ negotiationId: string,
137
+ ): void {
138
+ try {
139
+ const providerId = offer._provider?.participantId || "";
140
+ const assetId = offer["@id"] || offer.id;
141
+
142
+ if (!assetId) return;
143
+
144
+ const existingContracts = DSPContractStorage.getByAssetAndProvider(
145
+ assetId,
146
+ providerId,
147
+ );
148
+ const existingContract = existingContracts.find(
149
+ (c) => c.contractId === negotiationId || c.agreementId === contractId,
150
+ );
151
+
152
+ // Extract index endpoint URL from asset (dcat:endpointUrl)
153
+ const indexEndpointUrl = offer["dcat:endpointUrl"];
154
+
155
+ const assetName = offer.name || assetId || "Unknown Asset";
156
+
157
+ // Detect if this is an index asset
158
+ const isIndexAsset = assetName.toLowerCase().includes("index");
159
+
160
+ if (existingContract) {
161
+ // Update existing contract with index metadata
162
+ DSPContractStorage.updateState(existingContract.id, "FINALIZED", {
163
+ agreementId: contractId,
164
+ contractId: negotiationId,
165
+ isIndexAsset,
166
+ indexEndpointUrl,
167
+ });
168
+ } else {
169
+ // Create new contract entry
170
+ DSPContractStorage.create({
171
+ assetId,
172
+ datasetId: offer["@id"] || offer.id || "",
173
+ assetName,
174
+ assetDescription: offer.description,
175
+ providerName: offer._provider?.name || "Unknown Provider",
176
+ providerAddress: offer._provider?.address || "",
177
+ providerParticipantId: offer._provider?.participantId || "",
178
+ providerColor: offer._provider?.color,
179
+ policy: offer["odrl:hasPolicy"],
180
+ state: "FINALIZED",
181
+ contractId: negotiationId,
182
+ agreementId: contractId,
183
+ // Index-specific fields
184
+ isIndexAsset,
185
+ indexEndpointUrl,
186
+ });
187
+ }
188
+ } catch (error) {
189
+ console.error("[DSP Contract Catalog] Failed to save contract:", error);
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Update contract state in catalog (for failures)
195
+ */
196
+ export function updateContractState(
197
+ offer: DSPOffer,
198
+ negotiationId: string,
199
+ state: "FAILED" | "TERMINATED",
200
+ error?: string,
201
+ ): void {
202
+ try {
203
+ const providerId = offer._provider?.participantId || "";
204
+ const assetId = offer["@id"] || offer.id;
205
+
206
+ if (!assetId) return;
207
+
208
+ const existingContracts = DSPContractStorage.getByAssetAndProvider(
209
+ assetId,
210
+ providerId,
211
+ );
212
+ const existingContract = existingContracts.find(
213
+ (c) => c.contractId === negotiationId,
214
+ );
215
+
216
+ if (existingContract) {
217
+ DSPContractStorage.updateState(existingContract.id, state, { error });
218
+ }
219
+ } catch (error) {
220
+ console.error(
221
+ "[DSP Contract Catalog] Failed to update contract state:",
222
+ error,
223
+ );
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Delete all contracts for a specific asset and provider
229
+ */
230
+ export function deleteContractsForAsset(offer: DSPOffer): void {
231
+ const assetId = offer?.["@id"] || offer?.id;
232
+ const providerId = offer?._provider?.participantId || "";
233
+
234
+ if (assetId) {
235
+ const existingContracts = DSPContractStorage.getByAssetAndProvider(
236
+ assetId,
237
+ providerId,
238
+ );
239
+ for (const contract of existingContracts) {
240
+ DSPContractStorage.delete(contract.id);
241
+ }
242
+ }
243
+ }
@@ -0,0 +1,223 @@
1
+ import type { ODRLPolicy } from "@src/ds4go";
2
+ import { html, nothing } from "lit";
3
+ import { msg, str } from "@lit/localize";
4
+
5
+ /**
6
+ * Extract policies from various formats (single policy, array, or object with numeric keys)
7
+ */
8
+ export function extractPolicies(
9
+ policy: ODRLPolicy | ODRLPolicy[] | Record<string, any> | undefined,
10
+ ): { policies: ODRLPolicy[]; defaultPolicy: ODRLPolicy | undefined } {
11
+ let policies: ODRLPolicy[] = [];
12
+ let defaultPolicy: ODRLPolicy | undefined;
13
+
14
+ if (!policy) {
15
+ return { policies: [], defaultPolicy: undefined };
16
+ }
17
+
18
+ // Handle array of policies
19
+ if (Array.isArray(policy)) {
20
+ const target = (policy as any).target;
21
+ policies = policy.filter(
22
+ (item: any) => item && typeof item === "object" && item["@id"],
23
+ );
24
+
25
+ // Add target to each policy if it exists and policy doesn't have one
26
+ if (target) {
27
+ policies = policies.map((p: any) => {
28
+ if (!p.target && !p["odrl:target"]) {
29
+ return { ...p, target, "odrl:target": target };
30
+ }
31
+ return p;
32
+ });
33
+ }
34
+
35
+ defaultPolicy = policies.length > 0 ? policies[0] : undefined;
36
+ }
37
+ // Handle object with numeric keys (array-like object)
38
+ else if (typeof policy === "object" && policy !== null) {
39
+ const keys = Object.keys(policy);
40
+ const hasNumericKeys = keys.some((k) => /^\d+$/.test(k));
41
+
42
+ if (hasNumericKeys) {
43
+ const target = (policy as any).target;
44
+ const extractedPolicies: ODRLPolicy[] = [];
45
+
46
+ for (const key in policy) {
47
+ if (/^\d+$/.test(key)) {
48
+ let p = (policy as any)[key];
49
+ // Add target if it exists and policy doesn't have one
50
+ if (target && !p.target && !p["odrl:target"]) {
51
+ p = { ...p, target, "odrl:target": target };
52
+ }
53
+ extractedPolicies.push(p);
54
+ }
55
+ }
56
+
57
+ policies = extractedPolicies;
58
+ defaultPolicy = policies.length > 0 ? policies[0] : undefined;
59
+ } else {
60
+ // Single policy object
61
+ defaultPolicy = policy as ODRLPolicy;
62
+ policies = [defaultPolicy];
63
+ }
64
+ }
65
+
66
+ return { policies, defaultPolicy };
67
+ }
68
+
69
+ /**
70
+ * Validate that a policy doesn't have numeric keys (defensive check)
71
+ */
72
+ export function validatePolicyStructure(
73
+ policy: ODRLPolicy | undefined,
74
+ ): boolean {
75
+ if (!policy || typeof policy !== "object") {
76
+ return false;
77
+ }
78
+
79
+ const policyKeys = Object.keys(policy);
80
+ const hasNumericKeys = policyKeys.some((k) => /^\d+$/.test(k));
81
+
82
+ if (hasNumericKeys) {
83
+ console.error(
84
+ "[policyHelpers] ERROR: Policy has numeric keys! Invalid structure.",
85
+ policy,
86
+ );
87
+ return false;
88
+ }
89
+
90
+ return true;
91
+ }
92
+
93
+ /**
94
+ * Get policy by index from policies array
95
+ */
96
+ export function getPolicyByIndex(
97
+ policies: ODRLPolicy[],
98
+ index: number | undefined,
99
+ ): ODRLPolicy | undefined {
100
+ if (index === undefined || !policies || policies.length === 0) {
101
+ return undefined;
102
+ }
103
+ return policies[index];
104
+ }
105
+
106
+ /**
107
+ * Check if there are multiple policies available
108
+ */
109
+ export function hasMultiplePolicies(policies: ODRLPolicy[]): boolean {
110
+ return policies && policies.length > 1;
111
+ }
112
+
113
+ /**
114
+ * Format policy details for display
115
+ */
116
+ export function formatPolicyDetails(
117
+ policy: ODRLPolicy | undefined,
118
+ ): ReturnType<typeof html> {
119
+ if (!policy) return html`${msg("No policy details available")}`;
120
+
121
+ const permissions = policy["odrl:permission"];
122
+ const prohibitions = policy["odrl:prohibition"];
123
+ const obligations = policy["odrl:obligation"];
124
+
125
+ const permArray = permissions
126
+ ? Array.isArray(permissions)
127
+ ? permissions
128
+ : [permissions]
129
+ : [];
130
+ const prohibArray = prohibitions
131
+ ? Array.isArray(prohibitions)
132
+ ? prohibitions
133
+ : [prohibitions]
134
+ : [];
135
+ const obligArray = obligations
136
+ ? Array.isArray(obligations)
137
+ ? obligations
138
+ : [obligations]
139
+ : [];
140
+
141
+ return html`${policy["@id"]
142
+ ? html`<div class="policy-detail">
143
+ <strong>${msg("Policy ID:")}:</strong> ${policy["@id"]}
144
+ </div>`
145
+ : nothing}
146
+ ${policy["@type"]
147
+ ? html`<div class="policy-detail">
148
+ <strong>${msg("Type:")}:</strong> ${policy["@type"]}
149
+ </div>`
150
+ : nothing}
151
+ ${permArray.length > 0
152
+ ? html`<div class="policy-detail">
153
+ <strong>${msg("Permissions:")}:</strong>
154
+ <ul>
155
+ ${permArray.map(
156
+ (perm: any) => html`
157
+ <li>
158
+ ${msg(
159
+ str`Action: ${perm["odrl:action"]?.["@id"] || perm["odrl:action"] || msg("use")}`,
160
+ )}
161
+ </li>
162
+ ${perm["odrl:constraint"]
163
+ ? html`${(Array.isArray(perm["odrl:constraint"])
164
+ ? perm["odrl:constraint"]
165
+ : [perm["odrl:constraint"]]
166
+ ).map(
167
+ (c: any) => html`
168
+ <li style="margin-left: 20px;">
169
+ ${msg(
170
+ str`Constraint: ${c["odrl:leftOperand"]} ${c["odrl:operator"]} ${c["odrl:rightOperand"]}`,
171
+ )}
172
+ </li>
173
+ `,
174
+ )}`
175
+ : nothing}
176
+ `,
177
+ )}
178
+ </ul>
179
+ </div>`
180
+ : nothing}
181
+ ${prohibArray.length > 0
182
+ ? html`<div class="policy-detail">
183
+ <strong>${msg("Prohibitions:")}:</strong>
184
+ <ul>
185
+ ${prohibArray.map(
186
+ (prohib: any) => html`
187
+ <li>
188
+ ${msg(
189
+ str`Action: ${prohib["odrl:action"]?.["@id"] || prohib["odrl:action"] || msg("unknown")}`,
190
+ )}
191
+ </li>
192
+ `,
193
+ )}
194
+ </ul>
195
+ </div>`
196
+ : nothing}
197
+ ${obligArray.length > 0
198
+ ? html`<div class="policy-detail">
199
+ <strong>${msg("Obligations:")}:</strong>
200
+ <ul>
201
+ ${obligArray.map(
202
+ (oblig: any) => html`
203
+ <li>
204
+ ${msg(
205
+ str`Action: ${oblig["odrl:action"]?.["@id"] || oblig["odrl:action"] || msg("unknown")}`,
206
+ )}
207
+ </li>
208
+ `,
209
+ )}
210
+ </ul>
211
+ </div>`
212
+ : nothing}
213
+ ${policy.target
214
+ ? html`<div class="policy-detail">
215
+ <strong>${msg("Target Asset:")}:</strong> ${policy.target}
216
+ </div>`
217
+ : nothing}
218
+ ${policy.assigner
219
+ ? html`<div class="policy-detail">
220
+ <strong>${msg("Assigner:")}:</strong> ${policy.assigner}
221
+ </div>`
222
+ : nothing}`;
223
+ }
@@ -21,6 +21,10 @@ import requestNavigation from "@helpers/utils/requestNavigation";
21
21
  import uniq from "@helpers/utils/uniq";
22
22
  import CLIENT_CONTEXT from "@src/context.json";
23
23
 
24
+ // DSP helpers
25
+ import * as dspAgreementStorage from "@helpers/dsp/agreementStorage";
26
+ import * as dspPolicyHelpers from "@helpers/dsp/policyHelpers";
27
+
24
28
  export {
25
29
  CLIENT_CONTEXT,
26
30
  filterGenerator,
@@ -44,4 +48,7 @@ export {
44
48
  setupOnSaveReset,
45
49
  sort,
46
50
  uniq,
51
+ // DSP helpers
52
+ dspAgreementStorage,
53
+ dspPolicyHelpers,
47
54
  };
@@ -67,7 +67,7 @@ article {
67
67
  background-position: center center;
68
68
  box-sizing: border-box;
69
69
  width: 100%;
70
- padding: 16px;
70
+ padding: var(--scale-400);
71
71
  }
72
72
  main {
73
73
  display: flex;