Pattern:SubscribeWorkflow.create()… is framework-agnostic —
it fires DOM events (sub:caps-changed) that every framework can react to naturally.
Each section below is an independent island using a different rendering approach for the same
cap-gated content pattern.
Shared state bridge: All islands read from window.SubscriptionHub.getCaps()
on mount, then stay in sync via document.addEventListener('sub:caps-changed', …).
Alpine.js v3
Directive-based island
x-data + x-init component. Caps flow in via
sub:caps-changed → Alpine's reactivity handles all UI updates
automatically. Zero boilerplate.
🔓 محتوى حصري — أنت مشترك
هذا المحتوى مرئي فقط لمن لديه صلاحية read.
🔒
محتوى حصري — اشترك للوصول
caps:none
// Alpine.js integration pattern// Register once — reuse across any x-data islandfunctionsubscribeIsland(instanceId) {
return {
caps: [],
boot() {
// Hydrate from hub state on mountconst hub = window.SubscriptionHub;
if (hub) this.caps = [...hub.getCaps()];
// Stay in sync with hub via DOM event
document.addEventListener('sub:caps-changed', e => {
this.caps = [...e.detail.caps];
});
},
hasCap(cap) { return this.caps.includes(cap); },
openWorkflow() {
window.SubscribeWorkflow.create()
.title('اشترك للوصول')
.require('push', {
label: '🔔 اشترك مجاناً',
grants: ['read', 'book-download'],
})
.optional('email', { emailMode: 'form' })
.// onComplete: not needed — Alpine reacts to caps-changedshowModal();
},
};
}
// HTML (no JS in template):// <div x-data="subscribeIsland()" x-init="boot()">// <div x-show="hasCap('read')"> premium content </div>// <button x-show="!hasCap('read')" @click="openWorkflow()">// Subscribe</button>// </div>
@preact/signals-core
Signal primitives
No component tree — just signal() + effect().
Effects write directly to raw DOM nodes. Fine-grained: only nodes that
read a changed signal re-run.
// @preact/signals-core — no component tree needed// Works in any JS context: vanilla, Alpine, Lit, etc.import { signal, computed, effect }
from'@preact/signals-core';
// ── State ─────────────────────────────────const caps = signal([]);
const hasRead = computed(() =>
caps.value.includes('read'));
// ── Sync from hub ──────────────────────────const hub = window.SubscriptionHub;
if (hub) caps.value = [...hub.getCaps()];
document.addEventListener('sub:caps-changed', e => {
caps.value = [...e.detail.caps]; // triggers all effects
});
// ── Effects: update DOM when signals change ─effect(() => {
gate.style.display = hasRead.value ? 'none' : 'block';
content.style.display = hasRead.value ? 'block' : 'none';
});
effect(() => {
capsList.innerHTML = caps.value.length
? caps.value.map(c =>
`<span class="cap-chip active">${c}</span>`
).join('')
: '<span style="color:var(--muted)">none</span>';
});
// ── Workflow trigger ───────────────────────
btn.addEventListener('click', () => {
window.SubscribeWorkflow.create()
.require('push', {
grants: ['read', 'notifications'],
label: '🔔 اشترك مجاناً',
})
.optional('email', { emailMode: 'form' })
.showModal();
// No onComplete needed — caps signal updates// automatically via sub:caps-changed
});
Lit v3
Web component
<subscribe-cta-lit> — drop it anywhere, no framework
needed at the call site. Uses light DOM (no shadow root) so GateCard
styles bleed in naturally. <slot> for slotted content.
محتوى مقفل — اشترك للوصول
🔓 مرحباً! أنت مشترك
هذا المحتوى مرئي فقط لمن لديه صلاحية read.