feat(opsserver,web): replace the Angular UI and REST management layer with a TypedRequest-based ops server and bundled web frontend

This commit is contained in:
2026-03-20 16:43:44 +00:00
parent 0fc74ff995
commit d4f758ce0f
159 changed files with 12465 additions and 14861 deletions

8
ts_web/elements/index.ts Normal file
View File

@@ -0,0 +1,8 @@
export * from './shared/index.js';
export * from './sg-app-shell.js';
export * from './sg-view-dashboard.js';
export * from './sg-view-organizations.js';
export * from './sg-view-packages.js';
export * from './sg-view-tokens.js';
export * from './sg-view-settings.js';
export * from './sg-view-admin.js';

View File

@@ -0,0 +1,289 @@
import * as plugins from '../plugins.js';
import * as appstate from '../appstate.js';
import * as interfaces from '../../ts_interfaces/index.js';
import { appRouter } from '../router.js';
import {
DeesElement,
customElement,
html,
state,
css,
cssManager,
type TemplateResult,
} from '@design.estate/dees-element';
import type { SgViewDashboard } from './sg-view-dashboard.js';
import type { SgViewOrganizations } from './sg-view-organizations.js';
import type { SgViewPackages } from './sg-view-packages.js';
import type { SgViewTokens } from './sg-view-tokens.js';
import type { SgViewSettings } from './sg-view-settings.js';
import type { SgViewAdmin } from './sg-view-admin.js';
@customElement('sg-app-shell')
export class SgAppShell extends DeesElement {
@state()
accessor loginState: appstate.ILoginState = { identity: null, isLoggedIn: false };
@state()
accessor uiState: appstate.IUiState = { activeView: 'dashboard' };
@state()
accessor loginLoading: boolean = false;
@state()
accessor loginError: string = '';
@state()
accessor authProviders: interfaces.data.IPublicAuthProvider[] = [];
@state()
accessor localAuthEnabled: boolean = true;
private viewTabs = [
{
name: 'Dashboard',
iconName: 'lucide:layoutDashboard',
element: (async () => (await import('./sg-view-dashboard.js')).SgViewDashboard)(),
},
{
name: 'Organizations',
iconName: 'lucide:building2',
element: (async () => (await import('./sg-view-organizations.js')).SgViewOrganizations)(),
},
{
name: 'Packages',
iconName: 'lucide:package',
element: (async () => (await import('./sg-view-packages.js')).SgViewPackages)(),
},
{
name: 'Tokens',
iconName: 'lucide:key',
element: (async () => (await import('./sg-view-tokens.js')).SgViewTokens)(),
},
{
name: 'Settings',
iconName: 'lucide:settings',
element: (async () => (await import('./sg-view-settings.js')).SgViewSettings)(),
},
{
name: 'Admin',
iconName: 'lucide:shield',
element: (async () => (await import('./sg-view-admin.js')).SgViewAdmin)(),
},
];
private resolvedViewTabs: Array<{ name: string; iconName?: string; element: any }> = [];
constructor() {
super();
document.title = 'Stack.Gallery Registry';
// Make appRouter globally accessible for view elements
(globalThis as any).__sgAppRouter = appRouter;
const loginSubscription = appstate.loginStatePart
.select((s) => s)
.subscribe((loginState) => {
this.loginState = loginState;
});
this.rxSubscriptions.push(loginSubscription);
const uiSubscription = appstate.uiStatePart
.select((s) => s)
.subscribe((uiState) => {
this.uiState = uiState;
this.syncAppdashView(uiState.activeView);
});
this.rxSubscriptions.push(uiSubscription);
}
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
width: 100%;
height: 100%;
}
.maincontainer {
width: 100%;
height: 100vh;
}
`,
];
public render(): TemplateResult {
if (!this.loginState.isLoggedIn) {
return html`
<div class="maincontainer">
<sg-login-view
.providers=${this.authProviders}
.localAuthEnabled=${this.localAuthEnabled}
.loading=${this.loginLoading}
.error=${this.loginError}
@login=${(e: CustomEvent) => this.handleLocalLogin(e)}
@oauth-login=${(e: CustomEvent) => this.handleOAuthLogin(e)}
@ldap-login=${(e: CustomEvent) => this.handleLdapLogin(e)}
></sg-login-view>
</div>
`;
}
return html`
<div class="maincontainer">
<dees-simple-appdash
name="Stack.Gallery"
.viewTabs=${this.resolvedViewTabs}
.selectedView=${this.resolvedViewTabs.find(
(t) => t.name.toLowerCase().replace(/\s+/g, '-') === this.uiState.activeView,
) || this.resolvedViewTabs[0]}
>
</dees-simple-appdash>
</div>
`;
}
public async firstUpdated() {
// Fetch auth providers for login page
this.fetchAuthProviders();
// Resolve async view tab imports
const allTabs = await Promise.all(
this.viewTabs.map(async (tab) => ({
name: tab.name,
iconName: tab.iconName,
element: await tab.element,
})),
);
// Filter admin tab based on user role
this.resolvedViewTabs = this.loginState.identity?.isSystemAdmin
? allTabs
: allTabs.filter((t) => t.name !== 'Admin');
this.requestUpdate();
await this.updateComplete;
const appDash = this.shadowRoot!.querySelector('dees-simple-appdash') as any;
if (appDash) {
appDash.addEventListener('view-select', (e: CustomEvent) => {
const viewName = e.detail.view.name.toLowerCase().replace(/\s+/g, '-');
appRouter.navigateToView(viewName);
});
appDash.addEventListener('logout', async () => {
await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null);
});
// Load initial view
if (this.resolvedViewTabs.length > 0) {
const currentActiveView = appstate.uiStatePart.getState().activeView;
const initialView = this.resolvedViewTabs.find(
(t) => t.name.toLowerCase().replace(/\s+/g, '-') === currentActiveView,
) || this.resolvedViewTabs[0];
await appDash.loadView(initialView);
}
}
// Check for stored session
const loginState = appstate.loginStatePart.getState();
if (loginState.identity?.jwt) {
if (loginState.identity.expiresAt > Date.now()) {
// Validate token with server in the background
try {
await appstate.settingsStatePart.dispatchAction(appstate.fetchMeAction, null);
} catch {
console.warn('Stored session invalid, returning to login');
await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null);
}
} else {
// Token expired, try refresh
const newState = await appstate.loginStatePart.dispatchAction(
appstate.refreshTokenAction, null,
);
if (!newState.isLoggedIn) {
// Refresh failed
await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null);
}
}
}
}
private async fetchAuthProviders() {
try {
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_GetAuthProviders
>('/typedrequest', 'getAuthProviders');
const response = await typedRequest.fire({});
this.authProviders = response.providers;
this.localAuthEnabled = response.localAuthEnabled;
} catch {
// Default to local auth if we can't fetch providers
this.localAuthEnabled = true;
}
}
private async handleLocalLogin(e: CustomEvent) {
const { email, password } = e.detail;
this.loginLoading = true;
this.loginError = '';
try {
const newState = await appstate.loginStatePart.dispatchAction(appstate.loginAction, {
email,
password,
});
if (!newState.isLoggedIn) {
this.loginError = 'Invalid email or password';
}
} catch {
this.loginError = 'Login failed. Please try again.';
}
this.loginLoading = false;
}
private async handleOAuthLogin(e: CustomEvent) {
const { providerId } = e.detail;
try {
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_OAuthAuthorize
>('/typedrequest', 'oauthAuthorize');
const response = await typedRequest.fire({ providerId });
// Redirect to OAuth provider
window.location.href = response.redirectUrl;
} catch {
this.loginError = 'OAuth login failed. Please try again.';
}
}
private async handleLdapLogin(e: CustomEvent) {
const { providerId, username, password } = e.detail;
this.loginLoading = true;
this.loginError = '';
try {
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_LdapLogin
>('/typedrequest', 'ldapLogin');
const response = await typedRequest.fire({ providerId, username, password });
if (response.identity) {
appstate.loginStatePart.setState({
identity: response.identity,
isLoggedIn: true,
});
} else {
this.loginError = response.errorMessage || 'LDAP login failed';
}
} catch {
this.loginError = 'LDAP login failed. Please try again.';
}
this.loginLoading = false;
}
private syncAppdashView(viewName: string): void {
const appDash = this.shadowRoot?.querySelector('dees-simple-appdash') as any;
if (!appDash || this.resolvedViewTabs.length === 0) return;
const targetTab = this.resolvedViewTabs.find(
(t) => t.name.toLowerCase().replace(/\s+/g, '-') === viewName,
);
if (!targetTab) return;
appDash.loadView(targetTab);
}
}

View File

@@ -0,0 +1,100 @@
import * as appstate from '../appstate.js';
import * as shared from './shared/index.js';
import {
css,
cssManager,
customElement,
DeesElement,
html,
state,
type TemplateResult,
} from '@design.estate/dees-element';
@customElement('sg-view-admin')
export class SgViewAdmin extends DeesElement {
@state()
accessor adminState: appstate.IAdminState = { providers: [], platformSettings: null };
@state()
accessor editingProviderId: string | null = null;
constructor() {
super();
const sub = appstate.adminStatePart
.select((s) => s)
.subscribe((s) => {
this.adminState = s;
});
this.rxSubscriptions.push(sub);
}
public static styles = [
cssManager.defaultStyles,
shared.viewHostCss,
];
async connectedCallback() {
super.connectedCallback();
await appstate.adminStatePart.dispatchAction(appstate.fetchAdminProvidersAction, null);
await appstate.adminStatePart.dispatchAction(appstate.fetchPlatformSettingsAction, null);
}
public render(): TemplateResult {
if (this.editingProviderId !== null) {
const provider = this.editingProviderId
? this.adminState.providers.find((p) => p.id === this.editingProviderId) || null
: null;
return html`
<sg-admin-provider-form-view
.provider="${provider}"
@save="${(e: CustomEvent) => this.saveProvider(e.detail)}"
@cancel="${() => {
this.editingProviderId = null;
}}"
></sg-admin-provider-form-view>
`;
}
return html`
<sg-admin-providers-view
.providers="${this.adminState.providers}"
.settings="${this.adminState.platformSettings}"
@create="${() => {
this.editingProviderId = '';
}}"
@edit="${(e: CustomEvent) => {
this.editingProviderId = e.detail.providerId;
}}"
@delete="${(e: CustomEvent) => this.deleteProvider(e.detail.providerId)}"
@test="${(e: CustomEvent) => this.testProvider(e.detail.providerId)}"
@save-settings="${(e: CustomEvent) => this.saveSettings(e.detail.settings)}"
></sg-admin-providers-view>
`;
}
private async saveProvider(detail: any) {
// TODO: implement create/update provider
this.editingProviderId = null;
}
private async deleteProvider(providerId: string) {
await appstate.adminStatePart.dispatchAction(
appstate.deleteAdminProviderAction,
{ providerId },
);
}
private async testProvider(providerId: string) {
await appstate.adminStatePart.dispatchAction(
appstate.testAdminProviderAction,
{ providerId },
);
}
private async saveSettings(settings: any) {
await appstate.adminStatePart.dispatchAction(
appstate.updatePlatformSettingsAction,
{ auth: settings },
);
}
}

View File

@@ -0,0 +1,100 @@
import * as appstate from '../appstate.js';
import * as shared from './shared/index.js';
import {
css,
cssManager,
customElement,
DeesElement,
html,
state,
type TemplateResult,
} from '@design.estate/dees-element';
@customElement('sg-view-dashboard')
export class SgViewDashboard extends DeesElement {
@state()
accessor organizationsState: appstate.IOrganizationsState = {
organizations: [],
currentOrg: null,
repositories: [],
members: [],
};
@state()
accessor packagesState: appstate.IPackagesState = {
packages: [],
currentPackage: null,
versions: [],
total: 0,
query: '',
protocolFilter: '',
};
constructor() {
super();
const orgSub = appstate.organizationsStatePart
.select((s) => s)
.subscribe((s) => {
this.organizationsState = s;
});
this.rxSubscriptions.push(orgSub);
const pkgSub = appstate.packagesStatePart
.select((s) => s)
.subscribe((s) => {
this.packagesState = s;
});
this.rxSubscriptions.push(pkgSub);
}
public static styles = [
cssManager.defaultStyles,
shared.viewHostCss,
];
async connectedCallback() {
super.connectedCallback();
await appstate.organizationsStatePart.dispatchAction(appstate.fetchOrganizationsAction, null);
await appstate.packagesStatePart.dispatchAction(appstate.searchPackagesAction, { offset: 0 });
}
public render(): TemplateResult {
return html`
<sg-dashboard-view
.stats="${{
organizationCount: this.organizationsState.organizations.length,
packageCount: this.packagesState.total,
totalDownloads: 0,
tokenCount: 0,
}}"
.recentPackages="${this.packagesState.packages.slice(0, 5)}"
.organizations="${this.organizationsState.organizations}"
@navigate="${(e: CustomEvent) => this.handleNavigate(e)}"
></sg-dashboard-view>
`;
}
private handleNavigate(e: CustomEvent) {
const { type, id } = e.detail;
if (type === 'org' && id) {
const { appRouter } = await_import_router();
appRouter.navigateToEntity('organizations', id);
} else if (type === 'package' && id) {
const { appRouter } = await_import_router();
appRouter.navigateToEntity('packages', id);
} else if (type === 'packages') {
const { appRouter } = await_import_router();
appRouter.navigateToView('packages');
} else if (type === 'tokens') {
const { appRouter } = await_import_router();
appRouter.navigateToView('tokens');
}
}
}
// Lazy import to avoid circular dependency
function await_import_router() {
// Dynamic import not needed here since router is a separate module
// We use a workaround by importing at the module level
return { appRouter: (globalThis as any).__sgAppRouter };
}

View File

@@ -0,0 +1,127 @@
import * as appstate from '../appstate.js';
import * as shared from './shared/index.js';
import {
css,
cssManager,
customElement,
DeesElement,
html,
state,
type TemplateResult,
} from '@design.estate/dees-element';
@customElement('sg-view-organizations')
export class SgViewOrganizations extends DeesElement {
@state()
accessor organizationsState: appstate.IOrganizationsState = {
organizations: [],
currentOrg: null,
repositories: [],
members: [],
};
@state()
accessor uiState: appstate.IUiState = { activeView: 'organizations' };
constructor() {
super();
const orgSub = appstate.organizationsStatePart
.select((s) => s)
.subscribe((s) => {
this.organizationsState = s;
});
this.rxSubscriptions.push(orgSub);
const uiSub = appstate.uiStatePart
.select((s) => s)
.subscribe((s) => {
this.uiState = s;
});
this.rxSubscriptions.push(uiSub);
}
public static styles = [
cssManager.defaultStyles,
shared.viewHostCss,
];
async connectedCallback() {
super.connectedCallback();
await appstate.organizationsStatePart.dispatchAction(appstate.fetchOrganizationsAction, null);
// If there's an entity ID, load the detail
if (this.uiState.activeEntityId) {
await this.loadOrgDetail(this.uiState.activeEntityId);
}
}
private async loadOrgDetail(orgId: string) {
await appstate.organizationsStatePart.dispatchAction(
appstate.fetchOrganizationAction,
{ organizationId: orgId },
);
await appstate.organizationsStatePart.dispatchAction(
appstate.fetchRepositoriesAction,
{ organizationId: orgId },
);
await appstate.organizationsStatePart.dispatchAction(
appstate.fetchMembersAction,
{ organizationId: orgId },
);
}
public render(): TemplateResult {
if (this.uiState.activeEntityId && this.organizationsState.currentOrg) {
return html`
<sg-organization-detail-view
.organization="${this.organizationsState.currentOrg}"
.repositories="${this.organizationsState.repositories}"
.members="${this.organizationsState.members}"
@back="${() => this.goBack()}"
@select-repo="${(e: CustomEvent) => this.selectRepo(e.detail.repositoryId)}"
@create-repo="${() => {/* TODO: create repo modal */}}"
></sg-organization-detail-view>
`;
}
return html`
<sg-organizations-list-view
.organizations="${this.organizationsState.organizations}"
@select="${(e: CustomEvent) => this.selectOrg(e.detail.organizationId)}"
@create="${(e: CustomEvent) => this.createOrg(e.detail)}"
></sg-organizations-list-view>
`;
}
private selectOrg(orgId: string) {
appstate.uiStatePart.setState({
...appstate.uiStatePart.getState(),
activeEntityId: orgId,
});
this.loadOrgDetail(orgId);
}
private selectRepo(repoId: string) {
// Navigate to repository within org context
// For now, we could switch to packages view
}
private goBack() {
appstate.uiStatePart.setState({
...appstate.uiStatePart.getState(),
activeEntityId: undefined,
});
appstate.organizationsStatePart.setState({
...appstate.organizationsStatePart.getState(),
currentOrg: null,
repositories: [],
members: [],
});
}
private async createOrg(data: { name: string; displayName?: string; description?: string }) {
await appstate.organizationsStatePart.dispatchAction(
appstate.createOrganizationAction,
data,
);
}
}

View File

@@ -0,0 +1,156 @@
import * as appstate from '../appstate.js';
import * as shared from './shared/index.js';
import {
css,
cssManager,
customElement,
DeesElement,
html,
state,
type TemplateResult,
} from '@design.estate/dees-element';
@customElement('sg-view-packages')
export class SgViewPackages extends DeesElement {
@state()
accessor packagesState: appstate.IPackagesState = {
packages: [],
currentPackage: null,
versions: [],
total: 0,
query: '',
protocolFilter: '',
};
@state()
accessor uiState: appstate.IUiState = { activeView: 'packages' };
constructor() {
super();
const pkgSub = appstate.packagesStatePart
.select((s) => s)
.subscribe((s) => {
this.packagesState = s;
});
this.rxSubscriptions.push(pkgSub);
const uiSub = appstate.uiStatePart
.select((s) => s)
.subscribe((s) => {
this.uiState = s;
});
this.rxSubscriptions.push(uiSub);
}
public static styles = [
cssManager.defaultStyles,
shared.viewHostCss,
];
async connectedCallback() {
super.connectedCallback();
if (this.uiState.activeEntityId) {
await this.loadPackageDetail(this.uiState.activeEntityId);
} else {
await appstate.packagesStatePart.dispatchAction(
appstate.searchPackagesAction,
{ offset: 0 },
);
}
}
private async loadPackageDetail(packageId: string) {
await appstate.packagesStatePart.dispatchAction(
appstate.fetchPackageAction,
{ packageId },
);
await appstate.packagesStatePart.dispatchAction(
appstate.fetchPackageVersionsAction,
{ packageId },
);
}
public render(): TemplateResult {
if (this.uiState.activeEntityId && this.packagesState.currentPackage) {
return html`
<sg-package-detail-view
.package="${this.packagesState.currentPackage}"
.versions="${this.packagesState.versions}"
@back="${() => this.goBack()}"
@delete="${(e: CustomEvent) => this.deletePackage(e.detail.packageId)}"
@delete-version="${(e: CustomEvent) => this.deleteVersion(e.detail)}"
></sg-package-detail-view>
`;
}
return html`
<sg-packages-list-view
.packages="${this.packagesState.packages}"
.total="${this.packagesState.total}"
.query="${this.packagesState.query}"
.protocols="${['npm', 'oci', 'maven', 'cargo', 'pypi', 'composer', 'rubygems']}"
@search="${(e: CustomEvent) => this.search(e.detail.query)}"
@filter="${(e: CustomEvent) => this.filter(e.detail.protocol)}"
@select="${(e: CustomEvent) => this.selectPackage(e.detail.packageId)}"
@page="${(e: CustomEvent) => this.paginate(e.detail.offset)}"
></sg-packages-list-view>
`;
}
private selectPackage(packageId: string) {
appstate.uiStatePart.setState({
...appstate.uiStatePart.getState(),
activeEntityId: packageId,
});
this.loadPackageDetail(packageId);
}
private goBack() {
appstate.uiStatePart.setState({
...appstate.uiStatePart.getState(),
activeEntityId: undefined,
});
appstate.packagesStatePart.setState({
...appstate.packagesStatePart.getState(),
currentPackage: null,
versions: [],
});
}
private async search(query: string) {
await appstate.packagesStatePart.dispatchAction(
appstate.searchPackagesAction,
{ query, protocol: this.packagesState.protocolFilter, offset: 0 },
);
}
private async filter(protocol: string) {
await appstate.packagesStatePart.dispatchAction(
appstate.searchPackagesAction,
{ query: this.packagesState.query, protocol, offset: 0 },
);
}
private async paginate(offset: number) {
await appstate.packagesStatePart.dispatchAction(
appstate.searchPackagesAction,
{
query: this.packagesState.query,
protocol: this.packagesState.protocolFilter,
offset,
},
);
}
private async deletePackage(packageId: string) {
await appstate.packagesStatePart.dispatchAction(
appstate.deletePackageAction,
{ packageId },
);
this.goBack();
}
private async deleteVersion(detail: { packageId: string; version: string }) {
// TODO: implement deletePackageVersion action
}
}

View File

@@ -0,0 +1,67 @@
import * as appstate from '../appstate.js';
import * as shared from './shared/index.js';
import {
css,
cssManager,
customElement,
DeesElement,
html,
state,
type TemplateResult,
} from '@design.estate/dees-element';
@customElement('sg-view-settings')
export class SgViewSettings extends DeesElement {
@state()
accessor settingsState: appstate.ISettingsState = { user: null, sessions: [] };
constructor() {
super();
const sub = appstate.settingsStatePart
.select((s) => s)
.subscribe((s) => {
this.settingsState = s;
});
this.rxSubscriptions.push(sub);
}
public static styles = [
cssManager.defaultStyles,
shared.viewHostCss,
];
async connectedCallback() {
super.connectedCallback();
await appstate.settingsStatePart.dispatchAction(appstate.fetchMeAction, null);
await appstate.settingsStatePart.dispatchAction(appstate.fetchUserSessionsAction, null);
}
public render(): TemplateResult {
return html`
<sg-settings-view
.user="${this.settingsState.user}"
.sessions="${this.settingsState.sessions}"
@save-profile="${(e: CustomEvent) => this.saveProfile(e.detail)}"
@change-password="${(e: CustomEvent) => this.changePassword(e.detail)}"
@revoke-session="${(e: CustomEvent) => this.revokeSession(e.detail.sessionId)}"
@delete-account="${(e: CustomEvent) => this.deleteAccount(e.detail.password)}"
></sg-settings-view>
`;
}
private async saveProfile(detail: { displayName?: string; avatarUrl?: string }) {
await appstate.settingsStatePart.dispatchAction(appstate.updateProfileAction, detail);
}
private async changePassword(detail: { currentPassword: string; newPassword: string }) {
await appstate.settingsStatePart.dispatchAction(appstate.changePasswordAction, detail);
}
private async revokeSession(sessionId: string) {
await appstate.settingsStatePart.dispatchAction(appstate.revokeSessionAction, { sessionId });
}
private async deleteAccount(password: string) {
// TODO: implement delete account action
}
}

View File

@@ -0,0 +1,72 @@
import * as appstate from '../appstate.js';
import * as shared from './shared/index.js';
import {
css,
cssManager,
customElement,
DeesElement,
html,
state,
type TemplateResult,
} from '@design.estate/dees-element';
@customElement('sg-view-tokens')
export class SgViewTokens extends DeesElement {
@state()
accessor tokensState: appstate.ITokensState = { tokens: [] };
@state()
accessor organizationsState: appstate.IOrganizationsState = {
organizations: [],
currentOrg: null,
repositories: [],
members: [],
};
constructor() {
super();
const tokenSub = appstate.tokensStatePart
.select((s) => s)
.subscribe((s) => {
this.tokensState = s;
});
this.rxSubscriptions.push(tokenSub);
const orgSub = appstate.organizationsStatePart
.select((s) => s)
.subscribe((s) => {
this.organizationsState = s;
});
this.rxSubscriptions.push(orgSub);
}
public static styles = [
cssManager.defaultStyles,
shared.viewHostCss,
];
async connectedCallback() {
super.connectedCallback();
await appstate.tokensStatePart.dispatchAction(appstate.fetchTokensAction, {});
await appstate.organizationsStatePart.dispatchAction(appstate.fetchOrganizationsAction, null);
}
public render(): TemplateResult {
return html`
<sg-tokens-view
.tokens="${this.tokensState.tokens}"
.organizations="${this.organizationsState.organizations}"
@create="${(e: CustomEvent) => this.createToken(e.detail)}"
@revoke="${(e: CustomEvent) => this.revokeToken(e.detail.tokenId)}"
></sg-tokens-view>
`;
}
private async createToken(detail: any) {
await appstate.tokensStatePart.dispatchAction(appstate.createTokenAction, detail);
}
private async revokeToken(tokenId: string) {
await appstate.tokensStatePart.dispatchAction(appstate.revokeTokenAction, { tokenId });
}
}

View File

@@ -0,0 +1,12 @@
import { css } from '@design.estate/dees-element';
export const viewHostCss = css`
:host {
display: block;
width: 100%;
height: 100%;
overflow-y: auto;
padding: 24px;
box-sizing: border-box;
}
`;

View File

@@ -0,0 +1,2 @@
export * from './css.js';
export * from './sg-sectionheading.js';

View File

@@ -0,0 +1,43 @@
import {
css,
cssManager,
customElement,
DeesElement,
html,
type TemplateResult,
} from '@design.estate/dees-element';
declare global {
interface HTMLElementTagNameMap {
'sg-sectionheading': SgSectionheading;
}
}
@customElement('sg-sectionheading')
export class SgSectionheading extends DeesElement {
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
margin-bottom: 16px;
}
.heading {
font-size: 20px;
font-weight: 700;
color: ${cssManager.bdTheme('#111', '#fff')};
font-family: "JetBrains Mono", monospace;
padding-bottom: 8px;
border-bottom: 2px solid ${cssManager.bdTheme('#111', '#fff')};
}
`,
];
public render(): TemplateResult {
return html`
<div class="heading">
<slot></slot>
</div>
`;
}
}