refactor(sdk): split hooks, dsr-portal, provider, sync approaching 500 LOC
All four files split into focused sibling modules so every file lands
comfortably under the 300-LOC soft target (hard cap 500):
hooks.ts (474→43) → hooks-core / hooks-dsgvo / hooks-compliance
hooks-rag-security / hooks-ui
dsr-portal.ts (464→129) → dsr-portal-translations / dsr-portal-render
provider.tsx (462→247) → provider-effects / provider-callbacks
sync.ts (435→299) → sync-storage / sync-conflict
Zero behaviour changes. All public APIs remain importable from the
original paths (hooks.ts re-exports every hook, provider.tsx keeps all
named exports, sync.ts preserves StateSyncManager + factory).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,286 @@
|
||||
/**
|
||||
* Render helpers for <breakpilot-dsr-portal>
|
||||
*
|
||||
* Provides DSR_PORTAL_STYLES, buildFormHtml, and buildSuccessHtml.
|
||||
* Kept separate from the element class to stay under the 300-LOC target.
|
||||
*/
|
||||
|
||||
import type { DSRRequestType } from '@breakpilot/compliance-sdk-types'
|
||||
import { COMMON_STYLES } from './base'
|
||||
import type { DSRTranslations } from './dsr-portal-translations'
|
||||
|
||||
// =============================================================================
|
||||
// STYLES
|
||||
// =============================================================================
|
||||
|
||||
export const DSR_PORTAL_STYLES = `
|
||||
${COMMON_STYLES}
|
||||
|
||||
:host {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.portal {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0 0 10px;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 0 0 20px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.type-options {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.type-option {
|
||||
display: flex;
|
||||
padding: 15px;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
background: #fff;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.type-option:hover {
|
||||
border-color: #999;
|
||||
}
|
||||
|
||||
.type-option.selected {
|
||||
border-color: #1a1a1a;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.type-option input {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.type-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.type-description {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
font-size: 14px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
outline: none;
|
||||
border-color: #1a1a1a;
|
||||
}
|
||||
|
||||
.textarea {
|
||||
resize: vertical;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.error {
|
||||
padding: 12px;
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.btn-submit {
|
||||
width: 100%;
|
||||
padding: 12px 24px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
background: #1a1a1a;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-submit:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.disclaimer {
|
||||
margin-top: 20px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.success {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
background: #f0fdf4;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.success-title {
|
||||
margin: 0 0 10px;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
margin: 0;
|
||||
color: #166534;
|
||||
}
|
||||
`
|
||||
|
||||
// =============================================================================
|
||||
// HTML BUILDERS
|
||||
// =============================================================================
|
||||
|
||||
export interface FormRenderOptions {
|
||||
t: DSRTranslations
|
||||
selectedType: DSRRequestType | null
|
||||
name: string
|
||||
email: string
|
||||
additionalInfo: string
|
||||
isSubmitting: boolean
|
||||
error: string | null
|
||||
}
|
||||
|
||||
export function buildFormHtml(styles: string, opts: FormRenderOptions): string {
|
||||
const { t, selectedType, name, email, additionalInfo, isSubmitting, error } = opts
|
||||
|
||||
const types: DSRRequestType[] = [
|
||||
'ACCESS',
|
||||
'RECTIFICATION',
|
||||
'ERASURE',
|
||||
'PORTABILITY',
|
||||
'RESTRICTION',
|
||||
'OBJECTION',
|
||||
]
|
||||
|
||||
const typesHtml = types
|
||||
.map(
|
||||
type => `
|
||||
<label class="type-option ${selectedType === type ? 'selected' : ''}">
|
||||
<input
|
||||
type="radio"
|
||||
name="dsrType"
|
||||
value="${type}"
|
||||
${selectedType === type ? 'checked' : ''}
|
||||
/>
|
||||
<div>
|
||||
<div class="type-name">${t.types[type].name}</div>
|
||||
<div class="type-description">${t.types[type].description}</div>
|
||||
</div>
|
||||
</label>
|
||||
`
|
||||
)
|
||||
.join('')
|
||||
|
||||
const isValid = selectedType && email && name
|
||||
const isDisabled = !isValid || isSubmitting
|
||||
|
||||
return `
|
||||
<style>${styles}</style>
|
||||
<div class="portal">
|
||||
<h2 class="title">${t.title}</h2>
|
||||
<p class="subtitle">${t.subtitle}</p>
|
||||
|
||||
<form id="dsr-form">
|
||||
<div class="form-group">
|
||||
<label class="label">${t.requestType} *</label>
|
||||
<div class="type-options">
|
||||
${typesHtml}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="label">${t.name} *</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
id="name-input"
|
||||
value="${name}"
|
||||
placeholder="${t.namePlaceholder}"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="label">${t.email} *</label>
|
||||
<input
|
||||
type="email"
|
||||
class="input"
|
||||
id="email-input"
|
||||
value="${email}"
|
||||
placeholder="${t.emailPlaceholder}"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="label">${t.additionalInfo}</label>
|
||||
<textarea
|
||||
class="input textarea"
|
||||
id="info-input"
|
||||
placeholder="${t.additionalInfoPlaceholder}"
|
||||
rows="4"
|
||||
>${additionalInfo}</textarea>
|
||||
</div>
|
||||
|
||||
${error ? `<div class="error">${error}</div>` : ''}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="btn-submit"
|
||||
${isDisabled ? 'disabled' : ''}
|
||||
>
|
||||
${isSubmitting ? t.submitting : t.submit}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p class="disclaimer">${t.disclaimer}</p>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
export function buildSuccessHtml(styles: string, t: DSRTranslations, email: string): string {
|
||||
return `
|
||||
<style>${styles}</style>
|
||||
<div class="portal">
|
||||
<div class="success">
|
||||
<div class="success-icon">✓</div>
|
||||
<h2 class="success-title">${t.successTitle}</h2>
|
||||
<p class="success-message">
|
||||
${t.successMessage} ${email}.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
Reference in New Issue
Block a user