Initial commit: scaffold stack.gallery catalog frontend
This commit is contained in:
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# directories
|
||||||
|
node_modules/
|
||||||
|
dist_ts_web/
|
||||||
|
dist_watch/
|
||||||
|
.nogit/
|
||||||
|
.playwright-mcp/
|
||||||
|
|
||||||
|
# files
|
||||||
|
package-lock.json
|
||||||
17
html/index.html
Normal file
17
html/index.html
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="user-scalable=no, initial-scale=1">
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="preconnect" href="https://assetbroker.lossless.one/" crossorigin>
|
||||||
|
<link rel="stylesheet" href="https://assetbroker.lossless.one/fonts/fonts.css">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0px;
|
||||||
|
background: #222222;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script type="module" src="/bundle.js"></script>
|
||||||
|
</head>
|
||||||
|
<body></body>
|
||||||
|
</html>
|
||||||
18
html/index.ts
Normal file
18
html/index.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import * as deesWccTools from '@design.estate/dees-wcctools';
|
||||||
|
import * as elements from '../ts_web/elements/index.js';
|
||||||
|
import * as pages from '../ts_web/pages/index.js';
|
||||||
|
|
||||||
|
deesWccTools.setupWccTools({
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
name: 'Pages',
|
||||||
|
type: 'pages',
|
||||||
|
items: pages,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Elements',
|
||||||
|
type: 'elements',
|
||||||
|
items: elements,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
25
npmextra.json
Normal file
25
npmextra.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"@git.zone/tswatch": {
|
||||||
|
"preset": "element"
|
||||||
|
},
|
||||||
|
"@git.zone/cli": {
|
||||||
|
"projectType": "wcc",
|
||||||
|
"module": {
|
||||||
|
"githost": "code.foss.global",
|
||||||
|
"gitscope": "stack.gallery",
|
||||||
|
"gitrepo": "catalog",
|
||||||
|
"description": "UI component catalog for Stack.Gallery",
|
||||||
|
"npmPackagename": "@stack.gallery/catalog",
|
||||||
|
"license": "MIT",
|
||||||
|
"projectDomain": "stack.gallery"
|
||||||
|
},
|
||||||
|
"release": {
|
||||||
|
"registries": [
|
||||||
|
"https://verdaccio.lossless.digital",
|
||||||
|
"https://registry.npmjs.org"
|
||||||
|
],
|
||||||
|
"accessLevel": "public"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@ship.zone/szci": {}
|
||||||
|
}
|
||||||
44
package.json
Normal file
44
package.json
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"name": "@stack.gallery/catalog",
|
||||||
|
"version": "1.0.1",
|
||||||
|
"private": false,
|
||||||
|
"description": "UI component catalog for Stack.Gallery",
|
||||||
|
"main": "dist_ts_web/index.js",
|
||||||
|
"typings": "dist_ts_web/index.d.ts",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"test": "tstest test/",
|
||||||
|
"build": "tsbuild tsfolders --allowimplicitany && tsbundle element --production",
|
||||||
|
"watch": "tswatch"
|
||||||
|
},
|
||||||
|
"author": "Lossless GmbH",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@design.estate/dees-catalog": "^3.43.0",
|
||||||
|
"@design.estate/dees-domtools": "^2.3.8",
|
||||||
|
"@design.estate/dees-element": "^2.1.6",
|
||||||
|
"@design.estate/dees-wcctools": "^3.8.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@git.zone/tsbuild": "^4.1.2",
|
||||||
|
"@git.zone/tsbundle": "^2.8.3",
|
||||||
|
"@git.zone/tsrun": "^2.0.1",
|
||||||
|
"@git.zone/tstest": "^3.1.8",
|
||||||
|
"@git.zone/tswatch": "^3.1.0",
|
||||||
|
"@types/node": "^25.3.0"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"ts/**/*",
|
||||||
|
"ts_web/**/*",
|
||||||
|
"dist/**/*",
|
||||||
|
"dist_*/**/*",
|
||||||
|
"dist_ts/**/*",
|
||||||
|
"dist_ts_web/**/*",
|
||||||
|
"assets/**/*",
|
||||||
|
"npmextra.json",
|
||||||
|
"readme.md"
|
||||||
|
],
|
||||||
|
"browserslist": [
|
||||||
|
"last 1 Chrome versions"
|
||||||
|
]
|
||||||
|
}
|
||||||
9294
pnpm-lock.yaml
generated
Normal file
9294
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
0
readme.hints.md
Normal file
0
readme.hints.md
Normal file
9
ts_web/00_commitinfo_data.ts
Normal file
9
ts_web/00_commitinfo_data.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* autocreated commitance info file
|
||||||
|
* for: @stack.gallery/catalog
|
||||||
|
*/
|
||||||
|
export const commitinfo = {
|
||||||
|
name: '@stack.gallery/catalog',
|
||||||
|
version: '1.0.0',
|
||||||
|
description: 'UI component catalog for Stack.Gallery',
|
||||||
|
};
|
||||||
19
ts_web/elements/index.ts
Normal file
19
ts_web/elements/index.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
// Small reusable components
|
||||||
|
export * from './sg-stat-card.js';
|
||||||
|
export * from './sg-protocol-badge.js';
|
||||||
|
export * from './sg-install-snippet.js';
|
||||||
|
|
||||||
|
// View components
|
||||||
|
export * from './sg-login-view.js';
|
||||||
|
export * from './sg-dashboard-view.js';
|
||||||
|
export * from './sg-organizations-list-view.js';
|
||||||
|
export * from './sg-organization-detail-view.js';
|
||||||
|
export * from './sg-repository-detail-view.js';
|
||||||
|
export * from './sg-packages-list-view.js';
|
||||||
|
export * from './sg-package-detail-view.js';
|
||||||
|
export * from './sg-tokens-view.js';
|
||||||
|
export * from './sg-settings-view.js';
|
||||||
|
export * from './sg-admin-providers-view.js';
|
||||||
|
export * from './sg-admin-provider-form-view.js';
|
||||||
|
export * from './sg-public-layout.js';
|
||||||
|
export * from './sg-public-search-view.js';
|
||||||
674
ts_web/elements/sg-admin-provider-form-view.ts
Normal file
674
ts_web/elements/sg-admin-provider-form-view.ts
Normal file
@@ -0,0 +1,674 @@
|
|||||||
|
import {
|
||||||
|
DeesElement,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
css,
|
||||||
|
cssManager,
|
||||||
|
property,
|
||||||
|
type TemplateResult,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
import type { ISgAuthProviderDetail } from '../interfaces.js';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'sg-admin-provider-form-view': SgAdminProviderFormView;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IProviderFormData {
|
||||||
|
name: string;
|
||||||
|
displayName: string;
|
||||||
|
type: 'oidc' | 'ldap';
|
||||||
|
status: 'active' | 'disabled' | 'testing';
|
||||||
|
priority: number;
|
||||||
|
// OIDC fields
|
||||||
|
clientId?: string;
|
||||||
|
clientSecret?: string;
|
||||||
|
issuerUrl?: string;
|
||||||
|
authorizationUrl?: string;
|
||||||
|
tokenUrl?: string;
|
||||||
|
userInfoUrl?: string;
|
||||||
|
scopes?: string;
|
||||||
|
// LDAP fields
|
||||||
|
ldapUrl?: string;
|
||||||
|
bindDn?: string;
|
||||||
|
bindPassword?: string;
|
||||||
|
baseDn?: string;
|
||||||
|
userFilter?: string;
|
||||||
|
// Attribute mapping
|
||||||
|
usernameAttr?: string;
|
||||||
|
emailAttr?: string;
|
||||||
|
displayNameAttr?: string;
|
||||||
|
// Provisioning
|
||||||
|
autoCreateUsers?: boolean;
|
||||||
|
defaultRole?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement('sg-admin-provider-form-view')
|
||||||
|
export class SgAdminProviderFormView extends DeesElement {
|
||||||
|
public static demo = () => html`
|
||||||
|
<div style="padding: 24px; max-width: 800px; background: #09090b;">
|
||||||
|
<sg-admin-provider-form-view
|
||||||
|
.provider=${null}
|
||||||
|
></sg-admin-provider-form-view>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
public static demoGroups = ['Admin'];
|
||||||
|
|
||||||
|
@property({ type: Object })
|
||||||
|
public accessor provider: ISgAuthProviderDetail | null = null;
|
||||||
|
|
||||||
|
private formData: IProviderFormData = {
|
||||||
|
name: '',
|
||||||
|
displayName: '',
|
||||||
|
type: 'oidc',
|
||||||
|
status: 'testing',
|
||||||
|
priority: 10,
|
||||||
|
clientId: '',
|
||||||
|
clientSecret: '',
|
||||||
|
issuerUrl: '',
|
||||||
|
authorizationUrl: '',
|
||||||
|
tokenUrl: '',
|
||||||
|
userInfoUrl: '',
|
||||||
|
scopes: 'openid profile email',
|
||||||
|
ldapUrl: '',
|
||||||
|
bindDn: '',
|
||||||
|
bindPassword: '',
|
||||||
|
baseDn: '',
|
||||||
|
userFilter: '(uid={{username}})',
|
||||||
|
usernameAttr: 'preferred_username',
|
||||||
|
emailAttr: 'email',
|
||||||
|
displayNameAttr: 'name',
|
||||||
|
autoCreateUsers: true,
|
||||||
|
defaultRole: 'member',
|
||||||
|
};
|
||||||
|
|
||||||
|
async connectedCallback() {
|
||||||
|
await super.connectedCallback();
|
||||||
|
if (this.provider) {
|
||||||
|
this.formData = {
|
||||||
|
...this.formData,
|
||||||
|
name: this.provider.name,
|
||||||
|
displayName: this.provider.displayName,
|
||||||
|
type: this.provider.type,
|
||||||
|
status: this.provider.status,
|
||||||
|
priority: this.provider.priority,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#ddd', '#333')};
|
||||||
|
font-size: 13px;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-btn:hover {
|
||||||
|
border-color: ${cssManager.bdTheme('#999', '#666')};
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section */
|
||||||
|
.section {
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
padding: 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-subtitle {
|
||||||
|
font-size: 13px;
|
||||||
|
color: ${cssManager.bdTheme('#888', '#777')};
|
||||||
|
margin-top: -12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form elements */
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group.full {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#ddd')};
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#0a0a0a')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#ddd', '#333')};
|
||||||
|
font-size: 14px;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
outline: none;
|
||||||
|
font-family: inherit;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:focus {
|
||||||
|
border-color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input.mono {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: ${cssManager.bdTheme('#aaa', '#666')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-select {
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#0a0a0a')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#ddd', '#333')};
|
||||||
|
font-size: 14px;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
outline: none;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Type selector */
|
||||||
|
.type-selector {
|
||||||
|
display: flex;
|
||||||
|
gap: 0;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-btn {
|
||||||
|
padding: 10px 24px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-right: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-btn:last-child {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-btn.active {
|
||||||
|
background: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
color: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-btn:hover:not(.active) {
|
||||||
|
background: ${cssManager.bdTheme('#f5f5f5', '#1a1a1a')};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toggle */
|
||||||
|
.toggle-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch {
|
||||||
|
width: 44px;
|
||||||
|
height: 24px;
|
||||||
|
background: ${cssManager.bdTheme('#ddd', '#333')};
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
transition: background 150ms ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch.on {
|
||||||
|
background: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
background: #fff;
|
||||||
|
top: 3px;
|
||||||
|
left: 3px;
|
||||||
|
transition: transform 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch.on::after {
|
||||||
|
transform: translateX(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
.form-footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn {
|
||||||
|
padding: 10px 24px;
|
||||||
|
background: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
border: none;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn:hover {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-cancel-btn {
|
||||||
|
padding: 10px 24px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#ddd', '#333')};
|
||||||
|
font-size: 14px;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-cancel-btn:hover {
|
||||||
|
border-color: ${cssManager.bdTheme('#999', '#666')};
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
public render(): TemplateResult {
|
||||||
|
const isNew = !this.provider;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<div class="page-title">${isNew ? 'Add Authentication Provider' : 'Edit Provider'}</div>
|
||||||
|
<button class="cancel-btn" @click=${() => this.emitEvent('cancel', {})}>Cancel</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">Basic Information</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Provider Type</label>
|
||||||
|
<div class="type-selector">
|
||||||
|
<button
|
||||||
|
class="type-btn ${this.formData.type === 'oidc' ? 'active' : ''}"
|
||||||
|
@click=${() => this.updateField('type', 'oidc')}
|
||||||
|
>OpenID Connect</button>
|
||||||
|
<button
|
||||||
|
class="type-btn ${this.formData.type === 'ldap' ? 'active' : ''}"
|
||||||
|
@click=${() => this.updateField('type', 'ldap')}
|
||||||
|
>LDAP</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Internal Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-input mono"
|
||||||
|
.value=${this.formData.name}
|
||||||
|
@input=${(e: InputEvent) => this.updateField('name', (e.target as HTMLInputElement).value)}
|
||||||
|
placeholder="github-sso"
|
||||||
|
>
|
||||||
|
<span class="form-hint">Unique identifier (lowercase, no spaces)</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Display Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-input"
|
||||||
|
.value=${this.formData.displayName}
|
||||||
|
@input=${(e: InputEvent) => this.updateField('displayName', (e.target as HTMLInputElement).value)}
|
||||||
|
placeholder="GitHub SSO"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Status</label>
|
||||||
|
<select
|
||||||
|
class="form-select"
|
||||||
|
@change=${(e: Event) => this.updateField('status', (e.target as HTMLSelectElement).value)}
|
||||||
|
>
|
||||||
|
<option value="testing" ?selected=${this.formData.status === 'testing'}>Testing</option>
|
||||||
|
<option value="active" ?selected=${this.formData.status === 'active'}>Active</option>
|
||||||
|
<option value="disabled" ?selected=${this.formData.status === 'disabled'}>Disabled</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Priority</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
class="form-input"
|
||||||
|
.value=${String(this.formData.priority)}
|
||||||
|
@input=${(e: InputEvent) => this.updateField('priority', parseInt((e.target as HTMLInputElement).value) || 0)}
|
||||||
|
min="0"
|
||||||
|
>
|
||||||
|
<span class="form-hint">Lower number = higher priority</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${this.formData.type === 'oidc' ? this.renderOidcFields() : this.renderLdapFields()}
|
||||||
|
|
||||||
|
${this.renderAttributeMapping()}
|
||||||
|
|
||||||
|
${this.renderProvisioning()}
|
||||||
|
|
||||||
|
<div class="form-footer">
|
||||||
|
<button class="save-btn" @click=${this.handleSave}>
|
||||||
|
${isNew ? 'Create Provider' : 'Save Changes'}
|
||||||
|
</button>
|
||||||
|
<button class="footer-cancel-btn" @click=${() => this.emitEvent('cancel', {})}>Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderOidcFields(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">OpenID Connect Configuration</div>
|
||||||
|
<div class="section-subtitle">Configure your OIDC provider endpoints and credentials</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Client ID</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-input mono"
|
||||||
|
.value=${this.formData.clientId || ''}
|
||||||
|
@input=${(e: InputEvent) => this.updateField('clientId', (e.target as HTMLInputElement).value)}
|
||||||
|
placeholder="your-client-id"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Client Secret</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
class="form-input mono"
|
||||||
|
.value=${this.formData.clientSecret || ''}
|
||||||
|
@input=${(e: InputEvent) => this.updateField('clientSecret', (e.target as HTMLInputElement).value)}
|
||||||
|
placeholder="your-client-secret"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group full">
|
||||||
|
<label class="form-label">Issuer URL</label>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
class="form-input mono"
|
||||||
|
.value=${this.formData.issuerUrl || ''}
|
||||||
|
@input=${(e: InputEvent) => this.updateField('issuerUrl', (e.target as HTMLInputElement).value)}
|
||||||
|
placeholder="https://accounts.google.com"
|
||||||
|
>
|
||||||
|
<span class="form-hint">The OIDC discovery endpoint base URL</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Authorization URL (optional)</label>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
class="form-input mono"
|
||||||
|
.value=${this.formData.authorizationUrl || ''}
|
||||||
|
@input=${(e: InputEvent) => this.updateField('authorizationUrl', (e.target as HTMLInputElement).value)}
|
||||||
|
placeholder="Auto-discovered from issuer"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Token URL (optional)</label>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
class="form-input mono"
|
||||||
|
.value=${this.formData.tokenUrl || ''}
|
||||||
|
@input=${(e: InputEvent) => this.updateField('tokenUrl', (e.target as HTMLInputElement).value)}
|
||||||
|
placeholder="Auto-discovered from issuer"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">UserInfo URL (optional)</label>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
class="form-input mono"
|
||||||
|
.value=${this.formData.userInfoUrl || ''}
|
||||||
|
@input=${(e: InputEvent) => this.updateField('userInfoUrl', (e.target as HTMLInputElement).value)}
|
||||||
|
placeholder="Auto-discovered from issuer"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Scopes</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-input mono"
|
||||||
|
.value=${this.formData.scopes || ''}
|
||||||
|
@input=${(e: InputEvent) => this.updateField('scopes', (e.target as HTMLInputElement).value)}
|
||||||
|
placeholder="openid profile email"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderLdapFields(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">LDAP Configuration</div>
|
||||||
|
<div class="section-subtitle">Configure your LDAP/Active Directory server connection</div>
|
||||||
|
|
||||||
|
<div class="form-group full">
|
||||||
|
<label class="form-label">LDAP URL</label>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
class="form-input mono"
|
||||||
|
.value=${this.formData.ldapUrl || ''}
|
||||||
|
@input=${(e: InputEvent) => this.updateField('ldapUrl', (e.target as HTMLInputElement).value)}
|
||||||
|
placeholder="ldaps://ldap.example.com:636"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Bind DN</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-input mono"
|
||||||
|
.value=${this.formData.bindDn || ''}
|
||||||
|
@input=${(e: InputEvent) => this.updateField('bindDn', (e.target as HTMLInputElement).value)}
|
||||||
|
placeholder="cn=admin,dc=example,dc=com"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Bind Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
class="form-input mono"
|
||||||
|
.value=${this.formData.bindPassword || ''}
|
||||||
|
@input=${(e: InputEvent) => this.updateField('bindPassword', (e.target as HTMLInputElement).value)}
|
||||||
|
placeholder="Bind password"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group full">
|
||||||
|
<label class="form-label">Base DN</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-input mono"
|
||||||
|
.value=${this.formData.baseDn || ''}
|
||||||
|
@input=${(e: InputEvent) => this.updateField('baseDn', (e.target as HTMLInputElement).value)}
|
||||||
|
placeholder="ou=users,dc=example,dc=com"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group full">
|
||||||
|
<label class="form-label">User Search Filter</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-input mono"
|
||||||
|
.value=${this.formData.userFilter || ''}
|
||||||
|
@input=${(e: InputEvent) => this.updateField('userFilter', (e.target as HTMLInputElement).value)}
|
||||||
|
placeholder="(uid={{username}})"
|
||||||
|
>
|
||||||
|
<span class="form-hint">Use {{username}} as placeholder for the login username</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderAttributeMapping(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">Attribute Mapping</div>
|
||||||
|
<div class="section-subtitle">Map provider attributes to registry user fields</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Username Attribute</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-input mono"
|
||||||
|
.value=${this.formData.usernameAttr || ''}
|
||||||
|
@input=${(e: InputEvent) => this.updateField('usernameAttr', (e.target as HTMLInputElement).value)}
|
||||||
|
placeholder="preferred_username"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Email Attribute</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-input mono"
|
||||||
|
.value=${this.formData.emailAttr || ''}
|
||||||
|
@input=${(e: InputEvent) => this.updateField('emailAttr', (e.target as HTMLInputElement).value)}
|
||||||
|
placeholder="email"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Display Name Attribute</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-input mono"
|
||||||
|
.value=${this.formData.displayNameAttr || ''}
|
||||||
|
@input=${(e: InputEvent) => this.updateField('displayNameAttr', (e.target as HTMLInputElement).value)}
|
||||||
|
placeholder="name"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderProvisioning(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">Provisioning</div>
|
||||||
|
<div class="section-subtitle">Control automatic user creation and default roles</div>
|
||||||
|
|
||||||
|
<div class="toggle-row">
|
||||||
|
<div>
|
||||||
|
<div style="font-size: 14px; font-weight: 500; color: ${cssManager.bdTheme('#111', '#fff')}">Auto-create Users</div>
|
||||||
|
<div style="font-size: 12px; color: ${cssManager.bdTheme('#888', '#777')}">Automatically create accounts for new users who authenticate via this provider</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="toggle-switch ${this.formData.autoCreateUsers ? 'on' : ''}"
|
||||||
|
@click=${() => { this.formData.autoCreateUsers = !this.formData.autoCreateUsers; this.requestUpdate(); }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Default Role</label>
|
||||||
|
<select
|
||||||
|
class="form-select"
|
||||||
|
@change=${(e: Event) => this.updateField('defaultRole', (e.target as HTMLSelectElement).value)}
|
||||||
|
>
|
||||||
|
<option value="member" ?selected=${this.formData.defaultRole === 'member'}>Member</option>
|
||||||
|
<option value="admin" ?selected=${this.formData.defaultRole === 'admin'}>Admin</option>
|
||||||
|
</select>
|
||||||
|
<span class="form-hint">Role assigned to newly provisioned users</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateField(field: string, value: unknown) {
|
||||||
|
(this.formData as unknown as Record<string, unknown>)[field] = value;
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleSave() {
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('save', {
|
||||||
|
detail: { providerData: { ...this.formData } },
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private emitEvent(name: string, detail: Record<string, unknown>) {
|
||||||
|
this.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true }));
|
||||||
|
}
|
||||||
|
}
|
||||||
471
ts_web/elements/sg-admin-providers-view.ts
Normal file
471
ts_web/elements/sg-admin-providers-view.ts
Normal file
@@ -0,0 +1,471 @@
|
|||||||
|
import {
|
||||||
|
DeesElement,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
css,
|
||||||
|
cssManager,
|
||||||
|
property,
|
||||||
|
type TemplateResult,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
import type { ISgAuthProviderDetail, ISgPlatformSettings } from '../interfaces.js';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'sg-admin-providers-view': SgAdminProvidersView;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement('sg-admin-providers-view')
|
||||||
|
export class SgAdminProvidersView extends DeesElement {
|
||||||
|
public static demo = () => html`
|
||||||
|
<div style="padding: 24px; max-width: 1000px; background: #09090b;">
|
||||||
|
<sg-admin-providers-view
|
||||||
|
.providers=${[
|
||||||
|
{ id: 'p1', name: 'github', displayName: 'GitHub SSO', type: 'oidc', status: 'active', priority: 1, createdAt: '2025-08-01', updatedAt: '2026-03-15', lastTestedAt: '2026-03-15T10:00:00Z', lastTestResult: 'success' },
|
||||||
|
{ id: 'p2', name: 'corp-ldap', displayName: 'Corporate LDAP', type: 'ldap', status: 'active', priority: 2, createdAt: '2025-09-15', updatedAt: '2026-02-20', lastTestedAt: '2026-02-20T14:00:00Z', lastTestResult: 'success' },
|
||||||
|
{ id: 'p3', name: 'okta', displayName: 'Okta (Staging)', type: 'oidc', status: 'testing', priority: 3, createdAt: '2026-03-01', updatedAt: '2026-03-18', lastTestedAt: '2026-03-18T09:30:00Z', lastTestResult: 'failure', lastTestError: 'Invalid client_id' },
|
||||||
|
]}
|
||||||
|
.settings=${{
|
||||||
|
localAuthEnabled: true,
|
||||||
|
allowUserRegistration: false,
|
||||||
|
sessionDurationMinutes: 1440,
|
||||||
|
defaultProviderId: 'p1',
|
||||||
|
}}
|
||||||
|
></sg-admin-providers-view>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
public static demoGroups = ['Admin'];
|
||||||
|
|
||||||
|
@property({ type: Array })
|
||||||
|
public accessor providers: ISgAuthProviderDetail[] = [];
|
||||||
|
|
||||||
|
@property({ type: Object })
|
||||||
|
public accessor settings: ISgPlatformSettings = {
|
||||||
|
localAuthEnabled: true,
|
||||||
|
allowUserRegistration: false,
|
||||||
|
sessionDurationMinutes: 1440,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Platform settings */
|
||||||
|
.settings-box {
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
padding: 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-label {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: ${cssManager.bdTheme('#888', '#777')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch {
|
||||||
|
width: 44px;
|
||||||
|
height: 24px;
|
||||||
|
background: ${cssManager.bdTheme('#ddd', '#333')};
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
transition: background 150ms ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch.on {
|
||||||
|
background: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
background: #fff;
|
||||||
|
top: 3px;
|
||||||
|
left: 3px;
|
||||||
|
transition: transform 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch.on::after {
|
||||||
|
transform: translateX(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 0;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field {
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#0a0a0a')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#ddd', '#333')};
|
||||||
|
font-size: 14px;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
outline: none;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
width: 100px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field:focus {
|
||||||
|
border-color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-settings-btn {
|
||||||
|
align-self: flex-start;
|
||||||
|
padding: 8px 20px;
|
||||||
|
background: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
border: none;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-settings-btn:hover {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Providers section */
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
border: none;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-btn:hover {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Provider list */
|
||||||
|
.provider-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px;
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
border-bottom: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-name-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-name {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-type {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 1px 6px;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-type.oidc {
|
||||||
|
background: rgba(59, 130, 246, 0.15);
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-type.ldap {
|
||||||
|
background: rgba(168, 85, 247, 0.15);
|
||||||
|
color: #a855f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 1px 6px;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.active {
|
||||||
|
background: rgba(34, 197, 94, 0.15);
|
||||||
|
color: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.disabled {
|
||||||
|
background: rgba(239, 68, 68, 0.15);
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.testing {
|
||||||
|
background: rgba(234, 179, 8, 0.15);
|
||||||
|
color: #eab308;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-meta {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: ${cssManager.bdTheme('#888', '#777')};
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-result {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-result.success {
|
||||||
|
color: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-result.failure {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#ddd', '#333')};
|
||||||
|
font-size: 12px;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn:hover {
|
||||||
|
border-color: ${cssManager.bdTheme('#999', '#666')};
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn.delete {
|
||||||
|
border-color: rgba(239, 68, 68, 0.3);
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn.delete:hover {
|
||||||
|
background: rgba(239, 68, 68, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 48px 32px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: ${cssManager.bdTheme('#888', '#777')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
public render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<div class="container">
|
||||||
|
<div class="page-title">Authentication Management</div>
|
||||||
|
|
||||||
|
<div class="settings-box">
|
||||||
|
<div class="settings-title">Platform Settings</div>
|
||||||
|
|
||||||
|
<div class="toggle-row">
|
||||||
|
<div class="toggle-info">
|
||||||
|
<div class="toggle-label">Local Authentication</div>
|
||||||
|
<div class="toggle-hint">Allow users to log in with email and password</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="toggle-switch ${this.settings.localAuthEnabled ? 'on' : ''}"
|
||||||
|
@click=${() => { this.settings = { ...this.settings, localAuthEnabled: !this.settings.localAuthEnabled }; this.requestUpdate(); }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="toggle-row">
|
||||||
|
<div class="toggle-info">
|
||||||
|
<div class="toggle-label">User Registration</div>
|
||||||
|
<div class="toggle-hint">Allow new users to create accounts</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="toggle-switch ${this.settings.allowUserRegistration ? 'on' : ''}"
|
||||||
|
@click=${() => { this.settings = { ...this.settings, allowUserRegistration: !this.settings.allowUserRegistration }; this.requestUpdate(); }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-row">
|
||||||
|
<div class="toggle-info">
|
||||||
|
<div class="toggle-label">Session Duration</div>
|
||||||
|
<div class="toggle-hint">How long sessions remain valid (in minutes)</div>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
class="input-field"
|
||||||
|
.value=${String(this.settings.sessionDurationMinutes)}
|
||||||
|
@input=${(e: InputEvent) => {
|
||||||
|
this.settings = { ...this.settings, sessionDurationMinutes: parseInt((e.target as HTMLInputElement).value) || 0 };
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="save-settings-btn" @click=${this.handleSaveSettings}>Save Settings</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="section-title">Authentication Providers</div>
|
||||||
|
<button class="add-btn" @click=${() => this.emitEvent('create', {})}>+ Add Provider</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${this.providers.length > 0
|
||||||
|
? html`
|
||||||
|
<div class="provider-list">
|
||||||
|
${this.providers.map((provider) => this.renderProvider(provider))}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: html`<div class="empty-state">No authentication providers configured. Add an OIDC or LDAP provider to enable external authentication.</div>`}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderProvider(provider: ISgAuthProviderDetail): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<div class="provider-row">
|
||||||
|
<div class="provider-info">
|
||||||
|
<div class="provider-name-row">
|
||||||
|
<span class="provider-name">${provider.displayName}</span>
|
||||||
|
<span class="provider-type ${provider.type}">${provider.type}</span>
|
||||||
|
<span class="status-badge ${provider.status}">${provider.status}</span>
|
||||||
|
</div>
|
||||||
|
<div class="provider-meta">
|
||||||
|
<span>Priority: ${provider.priority}</span>
|
||||||
|
<span>Updated ${this.formatDate(provider.updatedAt)}</span>
|
||||||
|
${provider.lastTestedAt
|
||||||
|
? html`
|
||||||
|
<span>
|
||||||
|
Last test:
|
||||||
|
<span class="test-result ${provider.lastTestResult}">
|
||||||
|
${provider.lastTestResult}
|
||||||
|
</span>
|
||||||
|
${provider.lastTestError ? html` - ${provider.lastTestError}` : ''}
|
||||||
|
</span>
|
||||||
|
`
|
||||||
|
: ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="provider-actions">
|
||||||
|
<button class="action-btn" @click=${() => this.emitEvent('test', { providerId: provider.id })}>Test</button>
|
||||||
|
<button class="action-btn" @click=${() => this.emitEvent('edit', { providerId: provider.id })}>Edit</button>
|
||||||
|
<button class="action-btn delete" @click=${() => this.emitEvent('delete', { providerId: provider.id })}>Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleSaveSettings() {
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('save-settings', {
|
||||||
|
detail: { settings: { ...this.settings } },
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatDate(dateStr: string): string {
|
||||||
|
if (!dateStr) return '';
|
||||||
|
try {
|
||||||
|
return new Date(dateStr).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
|
||||||
|
} catch {
|
||||||
|
return dateStr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private emitEvent(name: string, detail: Record<string, unknown>) {
|
||||||
|
this.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true }));
|
||||||
|
}
|
||||||
|
}
|
||||||
400
ts_web/elements/sg-dashboard-view.ts
Normal file
400
ts_web/elements/sg-dashboard-view.ts
Normal file
@@ -0,0 +1,400 @@
|
|||||||
|
import {
|
||||||
|
DeesElement,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
css,
|
||||||
|
cssManager,
|
||||||
|
property,
|
||||||
|
type TemplateResult,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
import type { ISgDashboardStats, ISgPackage, ISgOrganization } from '../interfaces.js';
|
||||||
|
import './sg-protocol-badge.js';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'sg-dashboard-view': SgDashboardView;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement('sg-dashboard-view')
|
||||||
|
export class SgDashboardView extends DeesElement {
|
||||||
|
public static demo = () => html`
|
||||||
|
<div style="padding: 24px; max-width: 1200px; background: #09090b;">
|
||||||
|
<sg-dashboard-view
|
||||||
|
.stats=${{
|
||||||
|
organizationCount: 5,
|
||||||
|
packageCount: 128,
|
||||||
|
totalDownloads: 45230,
|
||||||
|
tokenCount: 12,
|
||||||
|
}}
|
||||||
|
.recentPackages=${[
|
||||||
|
{ id: '1', name: '@myorg/web-framework', protocol: 'npm', organizationId: 'org1', repositoryId: 'repo1', latestVersion: '3.2.1', isPrivate: false, downloadCount: 1240, updatedAt: '2026-03-19T10:30:00Z', description: 'Modern web framework' },
|
||||||
|
{ id: '2', name: 'myorg/api-gateway', protocol: 'oci', organizationId: 'org1', repositoryId: 'repo2', latestVersion: 'v1.8.0', isPrivate: true, downloadCount: 890, updatedAt: '2026-03-18T14:20:00Z', description: 'API gateway image' },
|
||||||
|
{ id: '3', name: 'data-utils', protocol: 'pypi', organizationId: 'org2', repositoryId: 'repo3', latestVersion: '0.9.4', isPrivate: false, downloadCount: 320, updatedAt: '2026-03-17T08:15:00Z' },
|
||||||
|
]}
|
||||||
|
.organizations=${[
|
||||||
|
{ id: 'org1', name: 'myorg', displayName: 'My Organization', isPublic: true, memberCount: 8, createdAt: '2025-06-01' },
|
||||||
|
{ id: 'org2', name: 'acme', displayName: 'ACME Corp', isPublic: false, memberCount: 25, createdAt: '2025-01-15' },
|
||||||
|
]}
|
||||||
|
></sg-dashboard-view>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
public static demoGroups = ['Dashboard'];
|
||||||
|
|
||||||
|
@property({ type: Object })
|
||||||
|
public accessor stats: ISgDashboardStats = {
|
||||||
|
organizationCount: 0,
|
||||||
|
packageCount: 0,
|
||||||
|
totalDownloads: 0,
|
||||||
|
tokenCount: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
@property({ type: Array })
|
||||||
|
public accessor recentPackages: ISgPackage[] = [];
|
||||||
|
|
||||||
|
@property({ type: Array })
|
||||||
|
public accessor organizations: ISgOrganization[] = [];
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stats grid */
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 1px;
|
||||||
|
background: ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 12px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
color: ${cssManager.bdTheme('#888', '#777')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section */
|
||||||
|
.section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-action {
|
||||||
|
font-size: 13px;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
|
cursor: pointer;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#ddd', '#333')};
|
||||||
|
padding: 6px 12px;
|
||||||
|
transition: all 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-action:hover {
|
||||||
|
border-color: ${cssManager.bdTheme('#999', '#666')};
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Package list */
|
||||||
|
.package-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
border-bottom: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 100ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-row:hover {
|
||||||
|
background: ${cssManager.bdTheme('#fafafa', '#1a1a1a')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: ${cssManager.bdTheme('#888', '#777')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-version {
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
|
background: ${cssManager.bdTheme('#f5f5f5', '#1a1a1a')};
|
||||||
|
padding: 2px 8px;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-downloads {
|
||||||
|
font-size: 12px;
|
||||||
|
color: ${cssManager.bdTheme('#888', '#777')};
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Orgs grid */
|
||||||
|
.orgs-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||||
|
gap: 1px;
|
||||||
|
background: ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.org-card {
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
padding: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 100ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.org-card:hover {
|
||||||
|
background: ${cssManager.bdTheme('#fafafa', '#1a1a1a')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.org-name {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.org-handle {
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
color: ${cssManager.bdTheme('#888', '#777')};
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.org-meta {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: ${cssManager.bdTheme('#888', '#777')};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Quick actions */
|
||||||
|
.quick-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-action-btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#ddd', '#333')};
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: ${cssManager.bdTheme('#333', '#ddd')};
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-action-btn:hover {
|
||||||
|
background: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
color: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
border-color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
padding: 32px;
|
||||||
|
text-align: center;
|
||||||
|
color: ${cssManager.bdTheme('#888', '#777')};
|
||||||
|
font-size: 14px;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
public render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<div class="dashboard">
|
||||||
|
<div class="page-title">Dashboard</div>
|
||||||
|
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">Organizations</div>
|
||||||
|
<div class="stat-value">${this.stats.organizationCount}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">Packages</div>
|
||||||
|
<div class="stat-value">${this.stats.packageCount}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">Total Downloads</div>
|
||||||
|
<div class="stat-value">${this.formatNumber(this.stats.totalDownloads)}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">API Tokens</div>
|
||||||
|
<div class="stat-value">${this.stats.tokenCount}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="section-title">Recent Packages</div>
|
||||||
|
<button class="section-action" @click=${() => this.navigate('packages')}>View all</button>
|
||||||
|
</div>
|
||||||
|
${this.recentPackages.length > 0
|
||||||
|
? html`
|
||||||
|
<div class="package-list">
|
||||||
|
${this.recentPackages.map(
|
||||||
|
(pkg) => html`
|
||||||
|
<div class="package-row" @click=${() => this.navigate('package', pkg.id)}>
|
||||||
|
<div class="package-info">
|
||||||
|
<div class="package-name">${pkg.name}</div>
|
||||||
|
<div class="package-meta">
|
||||||
|
<sg-protocol-badge .protocol=${pkg.protocol}></sg-protocol-badge>
|
||||||
|
${pkg.description || ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="package-right">
|
||||||
|
${pkg.latestVersion ? html`<span class="package-version">${pkg.latestVersion}</span>` : ''}
|
||||||
|
<span class="package-downloads">${this.formatNumber(pkg.downloadCount)} pulls</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: html`<div class="empty-state">No packages published yet</div>`}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="section-title">Organizations</div>
|
||||||
|
<button class="section-action" @click=${() => this.navigate('org')}>View all</button>
|
||||||
|
</div>
|
||||||
|
${this.organizations.length > 0
|
||||||
|
? html`
|
||||||
|
<div class="orgs-grid">
|
||||||
|
${this.organizations.map(
|
||||||
|
(org) => html`
|
||||||
|
<div class="org-card" @click=${() => this.navigate('org', org.id)}>
|
||||||
|
<div class="org-name">${org.displayName}</div>
|
||||||
|
<div class="org-handle">@${org.name}</div>
|
||||||
|
<div class="org-meta">
|
||||||
|
<span>${org.memberCount} members</span>
|
||||||
|
<span>${org.isPublic ? 'Public' : 'Private'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: html`<div class="empty-state">No organizations yet</div>`}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">Quick Actions</div>
|
||||||
|
<div class="quick-actions">
|
||||||
|
<button class="quick-action-btn" @click=${() => this.navigate('org')}>Browse Organizations</button>
|
||||||
|
<button class="quick-action-btn" @click=${() => this.navigate('packages')}>Browse Packages</button>
|
||||||
|
<button class="quick-action-btn" @click=${() => this.navigate('tokens')}>Manage Tokens</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatNumber(n: number): string {
|
||||||
|
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
||||||
|
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
|
||||||
|
return n.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private navigate(type: 'org' | 'package' | 'tokens' | 'packages', id?: string) {
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('navigate', {
|
||||||
|
detail: { type, id },
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
152
ts_web/elements/sg-install-snippet.ts
Normal file
152
ts_web/elements/sg-install-snippet.ts
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import {
|
||||||
|
DeesElement,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
property,
|
||||||
|
css,
|
||||||
|
cssManager,
|
||||||
|
type TemplateResult,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
import type { TSgProtocol } from '../interfaces.js';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'sg-install-snippet': SgInstallSnippet;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement('sg-install-snippet')
|
||||||
|
export class SgInstallSnippet extends DeesElement {
|
||||||
|
public static demo = () => html`
|
||||||
|
<div style="display: flex; flex-direction: column; gap: 16px; max-width: 500px;">
|
||||||
|
<sg-install-snippet .protocol=${'npm'} .packageName=${'@myorg/mypackage'}></sg-install-snippet>
|
||||||
|
<sg-install-snippet .protocol=${'oci'} .packageName=${'myorg/myimage'} .registryUrl=${'registry.stack.gallery'}></sg-install-snippet>
|
||||||
|
<sg-install-snippet .protocol=${'pypi'} .packageName=${'mypackage'} .registryUrl=${'registry.stack.gallery'}></sg-install-snippet>
|
||||||
|
<sg-install-snippet .protocol=${'cargo'} .packageName=${'mypackage'}></sg-install-snippet>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
public static demoGroups = ['Packages'];
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
accessor protocol: TSgProtocol = 'npm';
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
accessor packageName: string = '';
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
accessor registryUrl: string = '';
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.snippet {
|
||||||
|
background: ${cssManager.bdTheme('#f5f5f5', '#111')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
padding: 12px 16px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
color: ${cssManager.bdTheme('#333', '#e5e5e5')};
|
||||||
|
position: relative;
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.label {
|
||||||
|
font-size: 11px;
|
||||||
|
color: ${cssManager.bdTheme('#999', '#666')};
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
.copy-btn {
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background: ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
border: none;
|
||||||
|
padding: 4px 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 11px;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
|
}
|
||||||
|
.copy-btn:hover {
|
||||||
|
background: ${cssManager.bdTheme('#ddd', '#444')};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
private getInstallCommand(): { label: string; command: string } {
|
||||||
|
const reg = this.registryUrl;
|
||||||
|
switch (this.protocol) {
|
||||||
|
case 'npm':
|
||||||
|
return {
|
||||||
|
label: 'npm',
|
||||||
|
command: reg
|
||||||
|
? `npm install ${this.packageName} --registry=https://${reg}`
|
||||||
|
: `npm install ${this.packageName}`,
|
||||||
|
};
|
||||||
|
case 'oci':
|
||||||
|
return {
|
||||||
|
label: 'Docker / OCI',
|
||||||
|
command: reg
|
||||||
|
? `docker pull ${reg}/${this.packageName}`
|
||||||
|
: `docker pull ${this.packageName}`,
|
||||||
|
};
|
||||||
|
case 'maven':
|
||||||
|
return {
|
||||||
|
label: 'Maven',
|
||||||
|
command: `<dependency>\n <groupId>${this.packageName.split('/')[0] || ''}</groupId>\n <artifactId>${this.packageName.split('/')[1] || this.packageName}</artifactId>\n</dependency>`,
|
||||||
|
};
|
||||||
|
case 'cargo':
|
||||||
|
return {
|
||||||
|
label: 'Cargo',
|
||||||
|
command: `cargo add ${this.packageName}`,
|
||||||
|
};
|
||||||
|
case 'pypi':
|
||||||
|
return {
|
||||||
|
label: 'pip',
|
||||||
|
command: reg
|
||||||
|
? `pip install ${this.packageName} --index-url https://${reg}/simple/`
|
||||||
|
: `pip install ${this.packageName}`,
|
||||||
|
};
|
||||||
|
case 'composer':
|
||||||
|
return {
|
||||||
|
label: 'Composer',
|
||||||
|
command: `composer require ${this.packageName}`,
|
||||||
|
};
|
||||||
|
case 'rubygems':
|
||||||
|
return {
|
||||||
|
label: 'gem',
|
||||||
|
command: reg
|
||||||
|
? `gem install ${this.packageName} --source https://${reg}`
|
||||||
|
: `gem install ${this.packageName}`,
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return { label: this.protocol, command: this.packageName };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async copyToClipboard() {
|
||||||
|
const { command } = this.getInstallCommand();
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(command);
|
||||||
|
} catch {
|
||||||
|
// Fallback for browsers without clipboard API
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public render(): TemplateResult {
|
||||||
|
const { label, command } = this.getInstallCommand();
|
||||||
|
return html`
|
||||||
|
<div class="label">${label}</div>
|
||||||
|
<div class="snippet">
|
||||||
|
<code>${command}</code>
|
||||||
|
<button class="copy-btn" @click=${() => this.copyToClipboard()}>Copy</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
501
ts_web/elements/sg-login-view.ts
Normal file
501
ts_web/elements/sg-login-view.ts
Normal file
@@ -0,0 +1,501 @@
|
|||||||
|
import {
|
||||||
|
DeesElement,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
css,
|
||||||
|
cssManager,
|
||||||
|
property,
|
||||||
|
type TemplateResult,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
import type { ISgAuthProvider } from '../interfaces.js';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'sg-login-view': SgLoginView;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement('sg-login-view')
|
||||||
|
export class SgLoginView extends DeesElement {
|
||||||
|
public static demo = () => html`
|
||||||
|
<div style="height: 700px; display: flex; align-items: center; justify-content: center; background: #09090b;">
|
||||||
|
<sg-login-view
|
||||||
|
.providers=${[
|
||||||
|
{ id: 'github', name: 'github', displayName: 'GitHub', type: 'oidc' as const },
|
||||||
|
{ id: 'gitlab', name: 'gitlab', displayName: 'GitLab', type: 'oidc' as const },
|
||||||
|
{ id: 'corp-ldap', name: 'corp-ldap', displayName: 'Corporate LDAP', type: 'ldap' as const },
|
||||||
|
]}
|
||||||
|
.localAuthEnabled=${true}
|
||||||
|
.error=${''}
|
||||||
|
></sg-login-view>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
public static demoGroups = ['Auth'];
|
||||||
|
|
||||||
|
@property({ type: Array })
|
||||||
|
public accessor providers: ISgAuthProvider[] = [];
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
public accessor localAuthEnabled: boolean = true;
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
public accessor loading: boolean = false;
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
public accessor error: string = '';
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
background: ${cssManager.bdTheme('#f4f4f5', '#09090b')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 420px;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card {
|
||||||
|
background: ${cssManager.bdTheme('#ffffff', '#111')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
padding: 40px 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-section {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
background: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo svg {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
color: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-title {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
margin-bottom: 4px;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#ddd')};
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#0a0a0a')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#ddd', '#333')};
|
||||||
|
font-size: 14px;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 150ms ease;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:focus {
|
||||||
|
border-color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input::placeholder {
|
||||||
|
color: ${cssManager.bdTheme('#aaa', '#555')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input.has-error {
|
||||||
|
border-color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-banner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: rgba(239, 68, 68, 0.1);
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||||
|
font-size: 13px;
|
||||||
|
color: #f87171;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 20px;
|
||||||
|
background: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
border: none;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 150ms ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn:hover:not(:disabled) {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
border-top-color: currentColor;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.7s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider-line {
|
||||||
|
flex: 1;
|
||||||
|
height: 1px;
|
||||||
|
background: ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: ${cssManager.bdTheme('#999', '#666')};
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oauth-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oauth-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 16px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#ddd', '#333')};
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: ${cssManager.bdTheme('#333', '#ddd')};
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 150ms ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oauth-btn:hover {
|
||||||
|
background: ${cssManager.bdTheme('#f5f5f5', '#1a1a1a')};
|
||||||
|
border-color: ${cssManager.bdTheme('#ccc', '#555')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.ldap-section {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ldap-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ldap-toggle:hover {
|
||||||
|
color: ${cssManager.bdTheme('#333', '#ddd')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.ldap-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
padding-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ldap-provider-select {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ldap-provider-btn {
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#ddd', '#333')};
|
||||||
|
font-size: 12px;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ldap-provider-btn:hover,
|
||||||
|
.ldap-provider-btn.active {
|
||||||
|
background: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
color: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
border-color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
margin-top: 24px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
color: ${cssManager.bdTheme('#999', '#555')};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
private ldapExpanded = false;
|
||||||
|
private selectedLdapProviderId = '';
|
||||||
|
|
||||||
|
private get oauthProviders(): ISgAuthProvider[] {
|
||||||
|
return this.providers.filter((p) => p.type === 'oidc');
|
||||||
|
}
|
||||||
|
|
||||||
|
private get ldapProviders(): ISgAuthProvider[] {
|
||||||
|
return this.providers.filter((p) => p.type === 'ldap');
|
||||||
|
}
|
||||||
|
|
||||||
|
public render(): TemplateResult {
|
||||||
|
const hasOauth = this.oauthProviders.length > 0;
|
||||||
|
const hasLdap = this.ldapProviders.length > 0;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="login-container">
|
||||||
|
<div class="login-card">
|
||||||
|
<div class="logo-section">
|
||||||
|
<div class="logo">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path>
|
||||||
|
<polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline>
|
||||||
|
<line x1="12" y1="22.08" x2="12" y2="12"></line>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="brand-title">Stack.Gallery</div>
|
||||||
|
<div class="brand-subtitle">Sign in to your registry</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${this.error ? html`
|
||||||
|
<div class="error-banner">${this.error}</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
${this.localAuthEnabled ? html`
|
||||||
|
<form class="form" @submit=${this.handleLocalLogin}>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Email</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
id="login-email"
|
||||||
|
class="form-input ${this.error ? 'has-error' : ''}"
|
||||||
|
placeholder="you@example.com"
|
||||||
|
autocomplete="email"
|
||||||
|
?disabled=${this.loading}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="login-password"
|
||||||
|
class="form-input ${this.error ? 'has-error' : ''}"
|
||||||
|
placeholder="Enter your password"
|
||||||
|
autocomplete="current-password"
|
||||||
|
?disabled=${this.loading}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="submit-btn" ?disabled=${this.loading}>
|
||||||
|
${this.loading ? html`<div class="spinner"></div> Signing in...` : 'Sign in'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
${hasOauth ? html`
|
||||||
|
${this.localAuthEnabled ? html`
|
||||||
|
<div class="divider">
|
||||||
|
<span class="divider-line"></span>
|
||||||
|
<span class="divider-text">or continue with</span>
|
||||||
|
<span class="divider-line"></span>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
<div class="oauth-buttons">
|
||||||
|
${this.oauthProviders.map(
|
||||||
|
(p) => html`
|
||||||
|
<button
|
||||||
|
class="oauth-btn"
|
||||||
|
@click=${() => this.handleOAuthLogin(p.id)}
|
||||||
|
?disabled=${this.loading}
|
||||||
|
>
|
||||||
|
${p.displayName}
|
||||||
|
</button>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
${hasLdap ? html`
|
||||||
|
<div class="ldap-section">
|
||||||
|
${this.localAuthEnabled || hasOauth ? html`
|
||||||
|
<div class="divider">
|
||||||
|
<span class="divider-line"></span>
|
||||||
|
<span class="divider-text">enterprise</span>
|
||||||
|
<span class="divider-line"></span>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
<button class="ldap-toggle" @click=${() => { this.ldapExpanded = !this.ldapExpanded; this.requestUpdate(); }}>
|
||||||
|
${this.ldapExpanded ? '\u25BE' : '\u25B8'} Sign in with LDAP
|
||||||
|
</button>
|
||||||
|
${this.ldapExpanded ? html`
|
||||||
|
<div class="ldap-form">
|
||||||
|
${this.ldapProviders.length > 1 ? html`
|
||||||
|
<div class="ldap-provider-select">
|
||||||
|
${this.ldapProviders.map(
|
||||||
|
(p) => html`
|
||||||
|
<button
|
||||||
|
class="ldap-provider-btn ${this.selectedLdapProviderId === p.id ? 'active' : ''}"
|
||||||
|
@click=${() => { this.selectedLdapProviderId = p.id; this.requestUpdate(); }}
|
||||||
|
>${p.displayName}</button>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Username</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="ldap-username"
|
||||||
|
class="form-input"
|
||||||
|
placeholder="LDAP username"
|
||||||
|
autocomplete="username"
|
||||||
|
?disabled=${this.loading}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="ldap-password"
|
||||||
|
class="form-input"
|
||||||
|
placeholder="LDAP password"
|
||||||
|
autocomplete="current-password"
|
||||||
|
?disabled=${this.loading}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<button class="submit-btn" @click=${this.handleLdapLogin} ?disabled=${this.loading}>
|
||||||
|
${this.loading ? html`<div class="spinner"></div> Authenticating...` : 'Sign in with LDAP'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
Powered by Stack.Gallery
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleLocalLogin(e: Event) {
|
||||||
|
e.preventDefault();
|
||||||
|
const emailInput = this.shadowRoot?.getElementById('login-email') as HTMLInputElement;
|
||||||
|
const passwordInput = this.shadowRoot?.getElementById('login-password') as HTMLInputElement;
|
||||||
|
if (!emailInput || !passwordInput) return;
|
||||||
|
const email = emailInput.value.trim();
|
||||||
|
const password = passwordInput.value;
|
||||||
|
if (!email || !password) return;
|
||||||
|
this.dispatchEvent(new CustomEvent('login', {
|
||||||
|
detail: { email, password },
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleOAuthLogin(providerId: string) {
|
||||||
|
this.dispatchEvent(new CustomEvent('oauth-login', {
|
||||||
|
detail: { providerId },
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleLdapLogin() {
|
||||||
|
const usernameInput = this.shadowRoot?.getElementById('ldap-username') as HTMLInputElement;
|
||||||
|
const passwordInput = this.shadowRoot?.getElementById('ldap-password') as HTMLInputElement;
|
||||||
|
if (!usernameInput || !passwordInput) return;
|
||||||
|
const username = usernameInput.value.trim();
|
||||||
|
const password = passwordInput.value;
|
||||||
|
if (!username || !password) return;
|
||||||
|
const providerId = this.selectedLdapProviderId || this.ldapProviders[0]?.id;
|
||||||
|
if (!providerId) return;
|
||||||
|
this.dispatchEvent(new CustomEvent('ldap-login', {
|
||||||
|
detail: { providerId, username, password },
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
888
ts_web/elements/sg-organization-detail-view.ts
Normal file
888
ts_web/elements/sg-organization-detail-view.ts
Normal file
@@ -0,0 +1,888 @@
|
|||||||
|
import {
|
||||||
|
DeesElement,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
css,
|
||||||
|
cssManager,
|
||||||
|
property,
|
||||||
|
state,
|
||||||
|
type TemplateResult,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
import type {
|
||||||
|
ISgOrganizationDetail,
|
||||||
|
ISgRepository,
|
||||||
|
ISgOrganizationMember,
|
||||||
|
ISgOrgRedirect,
|
||||||
|
TSgOrgRole,
|
||||||
|
} from '../interfaces.js';
|
||||||
|
import './sg-protocol-badge.js';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'sg-organization-detail-view': SgOrganizationDetailView;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement('sg-organization-detail-view')
|
||||||
|
export class SgOrganizationDetailView extends DeesElement {
|
||||||
|
public static demo = () => html`
|
||||||
|
<div style="padding: 24px; max-width: 1200px; background: #09090b;">
|
||||||
|
<sg-organization-detail-view
|
||||||
|
.organization=${{
|
||||||
|
id: 'org1',
|
||||||
|
name: 'myorg',
|
||||||
|
displayName: 'My Organization',
|
||||||
|
description: 'Primary development organization for internal and open-source packages.',
|
||||||
|
isPublic: true,
|
||||||
|
memberCount: 8,
|
||||||
|
createdAt: '2025-06-01',
|
||||||
|
website: 'https://myorg.dev',
|
||||||
|
usedStorageBytes: 2147483648,
|
||||||
|
storageQuotaBytes: 10737418240,
|
||||||
|
}}
|
||||||
|
.repositories=${[
|
||||||
|
{ id: 'r1', organizationId: 'org1', name: 'npm-packages', description: 'NPM package repository', protocol: 'npm', visibility: 'public', isPublic: true, packageCount: 24, createdAt: '2025-06-15' },
|
||||||
|
{ id: 'r2', organizationId: 'org1', name: 'docker-images', description: 'OCI container images', protocol: 'oci', visibility: 'private', isPublic: false, packageCount: 12, createdAt: '2025-07-01' },
|
||||||
|
{ id: 'r3', organizationId: 'org1', name: 'python-libs', protocol: 'pypi', visibility: 'internal', isPublic: false, packageCount: 6, createdAt: '2025-08-10' },
|
||||||
|
]}
|
||||||
|
.members=${[
|
||||||
|
{ userId: 'u1', role: 'owner', addedAt: '2025-06-01', user: { username: 'admin', displayName: 'Admin User', avatarUrl: '' } },
|
||||||
|
{ userId: 'u2', role: 'admin', addedAt: '2025-06-10', user: { username: 'jane', displayName: 'Jane Doe', avatarUrl: '' } },
|
||||||
|
{ userId: 'u3', role: 'member', addedAt: '2025-07-05', user: { username: 'bob', displayName: 'Bob Smith', avatarUrl: '' } },
|
||||||
|
]}
|
||||||
|
></sg-organization-detail-view>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
public static demoGroups = ['Organizations'];
|
||||||
|
|
||||||
|
@property({ type: Object })
|
||||||
|
public accessor organization: ISgOrganizationDetail = {
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
displayName: '',
|
||||||
|
isPublic: true,
|
||||||
|
memberCount: 0,
|
||||||
|
createdAt: '',
|
||||||
|
usedStorageBytes: 0,
|
||||||
|
storageQuotaBytes: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
@property({ type: Array })
|
||||||
|
public accessor repositories: ISgRepository[] = [];
|
||||||
|
|
||||||
|
@property({ type: Array })
|
||||||
|
public accessor members: ISgOrganizationMember[] = [];
|
||||||
|
|
||||||
|
@property({ type: Array })
|
||||||
|
public accessor redirects: ISgOrgRedirect[] = [];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor editing: boolean = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor editName: string = '';
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor editDisplayName: string = '';
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor editDescription: string = '';
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor editWebsite: string = '';
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor editIsPublic: boolean = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor showDeleteConfirm: boolean = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor deleteConfirmName: string = '';
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Back button */
|
||||||
|
.back-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
font-size: 13px;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn:hover {
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Org header */
|
||||||
|
.org-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.org-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.org-avatar {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
background: ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.org-titles h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.org-handle {
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
color: ${cssManager.bdTheme('#888', '#777')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.org-description {
|
||||||
|
font-size: 14px;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#aaa')};
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.org-meta-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: ${cssManager.bdTheme('#888', '#777')};
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.org-meta-bar span {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.storage-bar {
|
||||||
|
width: 120px;
|
||||||
|
height: 4px;
|
||||||
|
background: ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.storage-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section */
|
||||||
|
.section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 6px 14px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#ddd', '#333')};
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: ${cssManager.bdTheme('#333', '#ddd')};
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-btn:hover {
|
||||||
|
background: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
color: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
border-color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Repo grid */
|
||||||
|
.repo-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
gap: 1px;
|
||||||
|
background: ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-card {
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
padding: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 100ms ease;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-card:hover {
|
||||||
|
background: ${cssManager.bdTheme('#fafafa', '#1a1a1a')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-name {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-description {
|
||||||
|
font-size: 13px;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#aaa')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-meta {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: ${cssManager.bdTheme('#888', '#777')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.visibility-tag {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
padding: 1px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visibility-tag.public {
|
||||||
|
background: rgba(34, 197, 94, 0.15);
|
||||||
|
color: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visibility-tag.private {
|
||||||
|
background: rgba(239, 68, 68, 0.15);
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visibility-tag.internal {
|
||||||
|
background: rgba(59, 130, 246, 0.15);
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Members list */
|
||||||
|
.members-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
border-bottom: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-avatar {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
background: ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-username {
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
color: ${cssManager.bdTheme('#888', '#777')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-badge {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
padding: 2px 8px;
|
||||||
|
background: ${cssManager.bdTheme('#f5f5f5', '#1a1a1a')};
|
||||||
|
color: ${cssManager.bdTheme('#666', '#aaa')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-btn {
|
||||||
|
padding: 4px 10px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||||
|
font-size: 12px;
|
||||||
|
color: #ef4444;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-btn:hover {
|
||||||
|
background: rgba(239, 68, 68, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 32px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: ${cssManager.bdTheme('#888', '#777')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header actions */
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-btn {
|
||||||
|
padding: 6px 14px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#ddd', '#333')};
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: ${cssManager.bdTheme('#333', '#ddd')};
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-btn:hover {
|
||||||
|
background: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
color: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
border-color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn {
|
||||||
|
padding: 6px 14px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #ef4444;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn:hover {
|
||||||
|
background: rgba(239, 68, 68, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Edit form */
|
||||||
|
.edit-form {
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
padding: 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-form-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#ddd')};
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input,
|
||||||
|
.form-textarea {
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#0a0a0a')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#ddd', '#333')};
|
||||||
|
font-size: 14px;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
outline: none;
|
||||||
|
font-family: inherit;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:focus,
|
||||||
|
.form-textarea:focus {
|
||||||
|
border-color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-toggle input[type="checkbox"] {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn {
|
||||||
|
padding: 8px 20px;
|
||||||
|
background: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
border: none;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn:hover {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-btn {
|
||||||
|
padding: 8px 20px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#ddd', '#333')};
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: ${cssManager.bdTheme('#333', '#ddd')};
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-btn:hover {
|
||||||
|
border-color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Danger zone */
|
||||||
|
.danger-zone {
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||||
|
padding: 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-zone-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-zone-text {
|
||||||
|
font-size: 13px;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#aaa')};
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-zone-warning {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-confirm-btn {
|
||||||
|
align-self: flex-start;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: #ef4444;
|
||||||
|
border: none;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-confirm-btn:hover {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-confirm-btn:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-cancel-btn {
|
||||||
|
align-self: flex-start;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#ddd', '#333')};
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: ${cssManager.bdTheme('#333', '#ddd')};
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-cancel-btn:hover {
|
||||||
|
border-color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
public render(): TemplateResult {
|
||||||
|
const storagePercent =
|
||||||
|
this.organization.storageQuotaBytes > 0
|
||||||
|
? Math.round((this.organization.usedStorageBytes / this.organization.storageQuotaBytes) * 100)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="container">
|
||||||
|
<button class="back-btn" @click=${() => this.emitEvent('back', {})}>
|
||||||
|
\u2190 Back to organizations
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="org-header">
|
||||||
|
<div class="org-top">
|
||||||
|
<div class="org-avatar">
|
||||||
|
${(this.organization.displayName || this.organization.name).charAt(0).toUpperCase()}
|
||||||
|
</div>
|
||||||
|
<div class="org-titles">
|
||||||
|
<h1>${this.organization.displayName || this.organization.name}</h1>
|
||||||
|
<div class="org-handle">@${this.organization.name}</div>
|
||||||
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<button class="edit-btn" @click=${() => this.startEdit()}>Edit</button>
|
||||||
|
<button class="delete-btn" @click=${() => { this.showDeleteConfirm = true; }}>Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${this.editing
|
||||||
|
? html`
|
||||||
|
<div class="edit-form">
|
||||||
|
<div class="edit-form-title">Edit Organization</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Handle</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-input"
|
||||||
|
.value=${this.editName}
|
||||||
|
@input=${(e: InputEvent) => { this.editName = (e.target as HTMLInputElement).value; }}
|
||||||
|
placeholder="my-org"
|
||||||
|
>
|
||||||
|
${this.editName !== this.organization.name
|
||||||
|
? html`<span class="form-hint" style="color: #f59e0b;">Renaming will create a redirect from "@${this.organization.name}"</span>`
|
||||||
|
: html`<span class="form-hint">Lowercase letters, numbers, and dashes</span>`}
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Display Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-input"
|
||||||
|
.value=${this.editDisplayName}
|
||||||
|
@input=${(e: InputEvent) => { this.editDisplayName = (e.target as HTMLInputElement).value; }}
|
||||||
|
placeholder="Organization display name"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Description</label>
|
||||||
|
<textarea
|
||||||
|
class="form-textarea"
|
||||||
|
.value=${this.editDescription}
|
||||||
|
@input=${(e: InputEvent) => { this.editDescription = (e.target as HTMLTextAreaElement).value; }}
|
||||||
|
placeholder="A short description"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Website</label>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
class="form-input"
|
||||||
|
.value=${this.editWebsite}
|
||||||
|
@input=${(e: InputEvent) => { this.editWebsite = (e.target as HTMLInputElement).value; }}
|
||||||
|
placeholder="https://..."
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-toggle">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
.checked=${this.editIsPublic}
|
||||||
|
@change=${(e: Event) => { this.editIsPublic = (e.target as HTMLInputElement).checked; }}
|
||||||
|
>
|
||||||
|
Public organization
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="save-btn" @click=${() => this.handleEditSave()}>Save</button>
|
||||||
|
<button class="cancel-btn" @click=${() => this.handleEditCancel()}>Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: html`
|
||||||
|
${this.organization.description
|
||||||
|
? html`<div class="org-description">${this.organization.description}</div>`
|
||||||
|
: ''}
|
||||||
|
<div class="org-meta-bar">
|
||||||
|
<span>${this.organization.memberCount} members</span>
|
||||||
|
<span>${this.organization.isPublic ? 'Public' : 'Private'}</span>
|
||||||
|
${this.organization.website ? html`<span>${this.organization.website}</span>` : ''}
|
||||||
|
<span>
|
||||||
|
Storage: ${this.formatBytes(this.organization.usedStorageBytes)} / ${this.formatBytes(this.organization.storageQuotaBytes)}
|
||||||
|
<span class="storage-bar"><span class="storage-fill" style="width: ${storagePercent}%"></span></span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="section-title">Repositories (${this.repositories.length})</div>
|
||||||
|
<button class="add-btn" @click=${() => this.emitEvent('create-repo', {})}>+ New Repository</button>
|
||||||
|
</div>
|
||||||
|
${this.repositories.length > 0
|
||||||
|
? html`
|
||||||
|
<div class="repo-grid">
|
||||||
|
${this.repositories.map(
|
||||||
|
(repo) => html`
|
||||||
|
<div class="repo-card" @click=${() => this.emitEvent('select-repo', { repositoryId: repo.id })}>
|
||||||
|
<div class="repo-name">
|
||||||
|
<sg-protocol-badge .protocol=${repo.protocol}></sg-protocol-badge>
|
||||||
|
${repo.name}
|
||||||
|
</div>
|
||||||
|
${repo.description ? html`<div class="repo-description">${repo.description}</div>` : ''}
|
||||||
|
<div class="repo-meta">
|
||||||
|
<span>${repo.packageCount} packages</span>
|
||||||
|
<span class="visibility-tag ${repo.visibility}">${repo.visibility}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: html`<div class="empty-state">No repositories yet. Create one to start publishing packages.</div>`}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="section-title">Members (${this.members.length})</div>
|
||||||
|
<button class="add-btn" @click=${() => this.emitEvent('add-member', {})}>+ Add Member</button>
|
||||||
|
</div>
|
||||||
|
${this.members.length > 0
|
||||||
|
? html`
|
||||||
|
<div class="members-list">
|
||||||
|
${this.members.map(
|
||||||
|
(member) => html`
|
||||||
|
<div class="member-row">
|
||||||
|
<div class="member-info">
|
||||||
|
<div class="member-avatar">
|
||||||
|
${(member.user?.displayName || member.user?.username || '?').charAt(0).toUpperCase()}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="member-name">${member.user?.displayName || 'Unknown'}</div>
|
||||||
|
<div class="member-username">@${member.user?.username || member.userId}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="member-actions">
|
||||||
|
<span class="role-badge">${member.role}</span>
|
||||||
|
${member.role !== 'owner'
|
||||||
|
? html`<button class="remove-btn" @click=${() => this.emitEvent('remove-member', { userId: member.userId })}>Remove</button>`
|
||||||
|
: ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: html`<div class="empty-state">No members</div>`}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${this.redirects.length > 0
|
||||||
|
? html`
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="section-title">Handle Redirects (${this.redirects.length})</div>
|
||||||
|
</div>
|
||||||
|
<div class="members-list">
|
||||||
|
${this.redirects.map(
|
||||||
|
(redirect) => html`
|
||||||
|
<div class="member-row">
|
||||||
|
<div class="member-info">
|
||||||
|
<div>
|
||||||
|
<div class="member-name" style="font-family: 'JetBrains Mono', monospace;">@${redirect.oldName}</div>
|
||||||
|
<div class="member-username">redirects to @${this.organization.name}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="member-actions">
|
||||||
|
<button class="remove-btn" @click=${() => this.emitEvent('delete-redirect', { redirectId: redirect.id })}>Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: ''}
|
||||||
|
|
||||||
|
${this.showDeleteConfirm
|
||||||
|
? html`
|
||||||
|
<div class="danger-zone">
|
||||||
|
<div class="danger-zone-title">Delete Organization</div>
|
||||||
|
<div class="danger-zone-text">
|
||||||
|
This action is <strong>irreversible</strong>. All repositories, packages, and data belonging to
|
||||||
|
<strong>@${this.organization.name}</strong> will be permanently deleted.
|
||||||
|
</div>
|
||||||
|
<div class="danger-zone-warning">
|
||||||
|
Type <strong>${this.organization.name}</strong> to confirm deletion.
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-input"
|
||||||
|
.value=${this.deleteConfirmName}
|
||||||
|
@input=${(e: InputEvent) => { this.deleteConfirmName = (e.target as HTMLInputElement).value; }}
|
||||||
|
placeholder=${this.organization.name}
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button
|
||||||
|
class="danger-confirm-btn"
|
||||||
|
?disabled=${this.deleteConfirmName !== this.organization.name}
|
||||||
|
@click=${() => this.handleDelete()}
|
||||||
|
>Delete Organization</button>
|
||||||
|
<button class="danger-cancel-btn" @click=${() => { this.showDeleteConfirm = false; this.deleteConfirmName = ''; }}>Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatBytes(bytes: number): string {
|
||||||
|
if (bytes === 0) return '0 B';
|
||||||
|
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||||
|
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private startEdit() {
|
||||||
|
this.editName = this.organization.name || '';
|
||||||
|
this.editDisplayName = this.organization.displayName || '';
|
||||||
|
this.editDescription = this.organization.description || '';
|
||||||
|
this.editWebsite = this.organization.website || '';
|
||||||
|
this.editIsPublic = this.organization.isPublic;
|
||||||
|
this.editing = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleEditSave() {
|
||||||
|
const detail: Record<string, unknown> = {
|
||||||
|
organizationId: this.organization.id,
|
||||||
|
displayName: this.editDisplayName.trim(),
|
||||||
|
description: this.editDescription.trim(),
|
||||||
|
website: this.editWebsite.trim(),
|
||||||
|
isPublic: this.editIsPublic,
|
||||||
|
};
|
||||||
|
// Only include name if it changed
|
||||||
|
if (this.editName.trim() && this.editName.trim() !== this.organization.name) {
|
||||||
|
detail.name = this.editName.trim();
|
||||||
|
}
|
||||||
|
this.emitEvent('edit', detail);
|
||||||
|
this.editing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleEditCancel() {
|
||||||
|
this.editing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleDelete() {
|
||||||
|
this.emitEvent('delete', {
|
||||||
|
organizationId: this.organization.id,
|
||||||
|
});
|
||||||
|
this.showDeleteConfirm = false;
|
||||||
|
this.deleteConfirmName = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private emitEvent(name: string, detail: Record<string, unknown>) {
|
||||||
|
this.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true }));
|
||||||
|
}
|
||||||
|
}
|
||||||
443
ts_web/elements/sg-organizations-list-view.ts
Normal file
443
ts_web/elements/sg-organizations-list-view.ts
Normal file
@@ -0,0 +1,443 @@
|
|||||||
|
import {
|
||||||
|
DeesElement,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
css,
|
||||||
|
cssManager,
|
||||||
|
property,
|
||||||
|
state,
|
||||||
|
type TemplateResult,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
import type { ISgOrganization } from '../interfaces.js';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'sg-organizations-list-view': SgOrganizationsListView;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement('sg-organizations-list-view')
|
||||||
|
export class SgOrganizationsListView extends DeesElement {
|
||||||
|
public static demo = () => html`
|
||||||
|
<div style="padding: 24px; max-width: 1200px; background: #09090b;">
|
||||||
|
<sg-organizations-list-view
|
||||||
|
.organizations=${[
|
||||||
|
{ id: 'org1', name: 'myorg', displayName: 'My Organization', description: 'Primary development organization', isPublic: true, memberCount: 12, createdAt: '2025-06-01' },
|
||||||
|
{ id: 'org2', name: 'acme-corp', displayName: 'ACME Corp', description: 'Enterprise packages and images', isPublic: false, memberCount: 48, createdAt: '2025-01-15' },
|
||||||
|
{ id: 'org3', name: 'oss-collective', displayName: 'Open Source Collective', isPublic: true, memberCount: 156, createdAt: '2024-11-20' },
|
||||||
|
]}
|
||||||
|
></sg-organizations-list-view>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
public static demoGroups = ['Organizations'];
|
||||||
|
|
||||||
|
@property({ type: Array })
|
||||||
|
public accessor organizations: ISgOrganization[] = [];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor showCreateForm: boolean = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor createName: string = '';
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor createDisplayName: string = '';
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor createDescription: string = '';
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
border: none;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-btn:hover {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-btn svg {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orgs-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||||
|
gap: 1px;
|
||||||
|
background: ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.org-card {
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
padding: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 100ms ease;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.org-card:hover {
|
||||||
|
background: ${cssManager.bdTheme('#fafafa', '#1a1a1a')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.org-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.org-avatar {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background: ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.org-titles {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.org-name {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.org-handle {
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
color: ${cssManager.bdTheme('#888', '#777')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.org-description {
|
||||||
|
font-size: 13px;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#aaa')};
|
||||||
|
line-height: 1.4;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.org-footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: ${cssManager.bdTheme('#888', '#777')};
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visibility-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1px 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visibility-badge.public {
|
||||||
|
background: rgba(34, 197, 94, 0.15);
|
||||||
|
color: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visibility-badge.private {
|
||||||
|
background: rgba(239, 68, 68, 0.15);
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 64px 32px;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: ${cssManager.bdTheme('#888', '#777')};
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Create form */
|
||||||
|
.create-form {
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
padding: 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-form-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#ddd')};
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input,
|
||||||
|
.form-textarea {
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#0a0a0a')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#ddd', '#333')};
|
||||||
|
font-size: 14px;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
outline: none;
|
||||||
|
font-family: inherit;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:focus,
|
||||||
|
.form-textarea:focus {
|
||||||
|
border-color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 60px;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: ${cssManager.bdTheme('#aaa', '#666')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn {
|
||||||
|
padding: 8px 20px;
|
||||||
|
background: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
border: none;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn:hover {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-btn {
|
||||||
|
padding: 8px 20px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#ddd', '#333')};
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: ${cssManager.bdTheme('#333', '#ddd')};
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-btn:hover {
|
||||||
|
border-color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
public render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<div class="page-title">Organizations</div>
|
||||||
|
<button class="create-btn" @click=${this.handleCreate}>
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||||
|
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||||
|
</svg>
|
||||||
|
New Organization
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${this.showCreateForm
|
||||||
|
? html`
|
||||||
|
<div class="create-form">
|
||||||
|
<div class="create-form-title">Create Organization</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Name *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-input"
|
||||||
|
.value=${this.createName}
|
||||||
|
@input=${(e: InputEvent) => { this.createName = (e.target as HTMLInputElement).value; }}
|
||||||
|
placeholder="my-org"
|
||||||
|
>
|
||||||
|
<span class="form-hint">Lowercase letters, numbers, and dashes only</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Display Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-input"
|
||||||
|
.value=${this.createDisplayName}
|
||||||
|
@input=${(e: InputEvent) => { this.createDisplayName = (e.target as HTMLInputElement).value; }}
|
||||||
|
placeholder="My Organization"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Description</label>
|
||||||
|
<textarea
|
||||||
|
class="form-textarea"
|
||||||
|
.value=${this.createDescription}
|
||||||
|
@input=${(e: InputEvent) => { this.createDescription = (e.target as HTMLTextAreaElement).value; }}
|
||||||
|
placeholder="A short description of the organization"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="submit-btn" @click=${this.handleCreateSubmit}>Create</button>
|
||||||
|
<button class="cancel-btn" @click=${this.handleCreateCancel}>Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: ''}
|
||||||
|
|
||||||
|
${this.organizations.length > 0
|
||||||
|
? html`
|
||||||
|
<div class="orgs-grid">
|
||||||
|
${this.organizations.map(
|
||||||
|
(org) => html`
|
||||||
|
<div class="org-card" @click=${() => this.handleSelect(org.id)}>
|
||||||
|
<div class="org-header">
|
||||||
|
<div class="org-avatar">${(org.displayName || org.name).charAt(0).toUpperCase()}</div>
|
||||||
|
<div class="org-titles">
|
||||||
|
<div class="org-name">${org.displayName || org.name}</div>
|
||||||
|
<div class="org-handle">@${org.name}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${org.description ? html`<div class="org-description">${org.description}</div>` : ''}
|
||||||
|
<div class="org-footer">
|
||||||
|
<span>${org.memberCount} member${org.memberCount !== 1 ? 's' : ''}</span>
|
||||||
|
<span class="visibility-badge ${org.isPublic ? 'public' : 'private'}">
|
||||||
|
${org.isPublic ? 'Public' : 'Private'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: html`
|
||||||
|
<div class="empty-state">
|
||||||
|
<div class="empty-title">No organizations yet</div>
|
||||||
|
<div class="empty-text">Create an organization to start publishing packages.</div>
|
||||||
|
<button class="create-btn" @click=${this.handleCreate}>Create Organization</button>
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleCreate() {
|
||||||
|
this.showCreateForm = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleCreateSubmit() {
|
||||||
|
if (!this.createName.trim()) return;
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('create', {
|
||||||
|
detail: {
|
||||||
|
name: this.createName.trim(),
|
||||||
|
displayName: this.createDisplayName.trim(),
|
||||||
|
description: this.createDescription.trim(),
|
||||||
|
},
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.createName = '';
|
||||||
|
this.createDisplayName = '';
|
||||||
|
this.createDescription = '';
|
||||||
|
this.showCreateForm = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleCreateCancel() {
|
||||||
|
this.createName = '';
|
||||||
|
this.createDisplayName = '';
|
||||||
|
this.createDescription = '';
|
||||||
|
this.showCreateForm = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleSelect(organizationId: string) {
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('select', {
|
||||||
|
detail: { organizationId },
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
496
ts_web/elements/sg-package-detail-view.ts
Normal file
496
ts_web/elements/sg-package-detail-view.ts
Normal file
@@ -0,0 +1,496 @@
|
|||||||
|
import {
|
||||||
|
DeesElement,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
css,
|
||||||
|
cssManager,
|
||||||
|
property,
|
||||||
|
type TemplateResult,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
import type { ISgPackageDetail, ISgPackageVersion } from '../interfaces.js';
|
||||||
|
import './sg-protocol-badge.js';
|
||||||
|
import './sg-install-snippet.js';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'sg-package-detail-view': SgPackageDetailView;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement('sg-package-detail-view')
|
||||||
|
export class SgPackageDetailView extends DeesElement {
|
||||||
|
public static demo = () => html`
|
||||||
|
<div style="padding: 24px; max-width: 1200px; background: #09090b;">
|
||||||
|
<sg-package-detail-view
|
||||||
|
.package=${{
|
||||||
|
id: 'p1',
|
||||||
|
name: '@myorg/web-framework',
|
||||||
|
description: 'Modern web framework for building full-stack TypeScript applications with first-class support for SSR, routing, and state management.',
|
||||||
|
protocol: 'npm',
|
||||||
|
organizationId: 'org1',
|
||||||
|
repositoryId: 'r1',
|
||||||
|
latestVersion: '3.2.1',
|
||||||
|
isPrivate: false,
|
||||||
|
downloadCount: 12400,
|
||||||
|
updatedAt: '2026-03-19T10:30:00Z',
|
||||||
|
distTags: { latest: '3.2.1', next: '4.0.0-beta.1' },
|
||||||
|
versions: ['3.2.1', '3.2.0', '3.1.0', '3.0.0', '2.5.0'],
|
||||||
|
starCount: 42,
|
||||||
|
storageBytes: 52428800,
|
||||||
|
createdAt: '2025-01-10T08:00:00Z',
|
||||||
|
}}
|
||||||
|
.versions=${[
|
||||||
|
{ version: '3.2.1', publishedAt: '2026-03-19T10:30:00Z', size: 1048576, downloads: 5200 },
|
||||||
|
{ version: '3.2.0', publishedAt: '2026-03-10T14:00:00Z', size: 1024000, downloads: 4800 },
|
||||||
|
{ version: '3.1.0', publishedAt: '2026-02-15T09:00:00Z', size: 998400, downloads: 1800 },
|
||||||
|
{ version: '3.0.0', publishedAt: '2026-01-01T12:00:00Z', size: 921600, downloads: 600 },
|
||||||
|
]}
|
||||||
|
></sg-package-detail-view>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
public static demoGroups = ['Packages'];
|
||||||
|
|
||||||
|
@property({ type: Object })
|
||||||
|
public accessor package: ISgPackageDetail = {
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
protocol: 'npm',
|
||||||
|
organizationId: '',
|
||||||
|
repositoryId: '',
|
||||||
|
isPrivate: false,
|
||||||
|
downloadCount: 0,
|
||||||
|
updatedAt: '',
|
||||||
|
distTags: {},
|
||||||
|
versions: [],
|
||||||
|
starCount: 0,
|
||||||
|
storageBytes: 0,
|
||||||
|
createdAt: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
@property({ type: Array })
|
||||||
|
public accessor versions: ISgPackageVersion[] = [];
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
font-size: 13px;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn:hover {
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Layout */
|
||||||
|
.content-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 300px;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.content-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Package header */
|
||||||
|
.pkg-header {
|
||||||
|
padding: 24px;
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pkg-title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pkg-name {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.private-badge {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 2px 6px;
|
||||||
|
background: rgba(239, 68, 68, 0.15);
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pkg-description {
|
||||||
|
font-size: 14px;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#aaa')};
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dist-tags {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dist-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
background: ${cssManager.bdTheme('#f5f5f5', '#1a1a1a')};
|
||||||
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.dist-tag-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar */
|
||||||
|
.sidebar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-section {
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
color: ${cssManager.bdTheme('#888', '#777')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-stat {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-stat-label {
|
||||||
|
color: ${cssManager.bdTheme('#888', '#777')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-stat-value {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sections */
|
||||||
|
.section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Version list */
|
||||||
|
.version-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
border-bottom: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-number {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-meta {
|
||||||
|
font-size: 12px;
|
||||||
|
color: ${cssManager.bdTheme('#888', '#777')};
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-version-btn {
|
||||||
|
padding: 4px 10px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||||
|
font-size: 11px;
|
||||||
|
color: #ef4444;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-version-btn:hover {
|
||||||
|
background: rgba(239, 68, 68, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Danger zone */
|
||||||
|
.danger-zone {
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-text {
|
||||||
|
font-size: 13px;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#aaa')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-btn {
|
||||||
|
align-self: flex-start;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #ef4444;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ef4444;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-btn:hover {
|
||||||
|
background: #ef4444;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
public render(): TemplateResult {
|
||||||
|
const pkg = this.package;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="container">
|
||||||
|
<button class="back-btn" @click=${() => this.emitEvent('back', {})}>
|
||||||
|
\u2190 Back to packages
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="content-grid">
|
||||||
|
<div class="main-content">
|
||||||
|
<div class="pkg-header">
|
||||||
|
<div class="pkg-title-row">
|
||||||
|
<sg-protocol-badge .protocol=${pkg.protocol}></sg-protocol-badge>
|
||||||
|
<span class="pkg-name">${pkg.name}</span>
|
||||||
|
${pkg.isPrivate ? html`<span class="private-badge">Private</span>` : ''}
|
||||||
|
</div>
|
||||||
|
${pkg.description ? html`<div class="pkg-description">${pkg.description}</div>` : ''}
|
||||||
|
${Object.keys(pkg.distTags).length > 0
|
||||||
|
? html`
|
||||||
|
<div class="dist-tags">
|
||||||
|
${Object.entries(pkg.distTags).map(
|
||||||
|
([tag, version]) => html`
|
||||||
|
<span class="dist-tag">
|
||||||
|
<span class="dist-tag-name">${tag}</span>: ${version}
|
||||||
|
</span>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section" style="margin-top: 24px;">
|
||||||
|
<div class="section-title">Install</div>
|
||||||
|
<sg-install-snippet
|
||||||
|
.protocol=${pkg.protocol}
|
||||||
|
.packageName=${pkg.name}
|
||||||
|
></sg-install-snippet>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section" style="margin-top: 24px;">
|
||||||
|
<div class="section-title">Versions (${this.versions.length})</div>
|
||||||
|
${this.versions.length > 0
|
||||||
|
? html`
|
||||||
|
<div class="version-list">
|
||||||
|
${this.versions.map(
|
||||||
|
(v) => html`
|
||||||
|
<div class="version-row">
|
||||||
|
<div class="version-info">
|
||||||
|
<span class="version-number">${v.version}</span>
|
||||||
|
<div class="version-meta">
|
||||||
|
<span>${this.formatDate(v.publishedAt)}</span>
|
||||||
|
<span>${this.formatBytes(v.size)}</span>
|
||||||
|
<span>${this.formatNumber(v.downloads)} downloads</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="version-actions">
|
||||||
|
<button
|
||||||
|
class="delete-version-btn"
|
||||||
|
@click=${() =>
|
||||||
|
this.emitEvent('delete-version', {
|
||||||
|
packageId: pkg.id,
|
||||||
|
version: v.version,
|
||||||
|
})}
|
||||||
|
>Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section" style="margin-top: 24px;">
|
||||||
|
<div class="danger-zone">
|
||||||
|
<div class="danger-title">Danger Zone</div>
|
||||||
|
<div class="danger-text">
|
||||||
|
Deleting this package will permanently remove all versions and associated data.
|
||||||
|
This action cannot be undone.
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="danger-btn"
|
||||||
|
@click=${() => this.emitEvent('delete', { packageId: pkg.id })}
|
||||||
|
>Delete Package</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar">
|
||||||
|
<div class="sidebar-section">
|
||||||
|
<div class="sidebar-title">Stats</div>
|
||||||
|
<div class="sidebar-stat">
|
||||||
|
<span class="sidebar-stat-label">Downloads</span>
|
||||||
|
<span class="sidebar-stat-value">${this.formatNumber(pkg.downloadCount)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-stat">
|
||||||
|
<span class="sidebar-stat-label">Stars</span>
|
||||||
|
<span class="sidebar-stat-value">${pkg.starCount}</span>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-stat">
|
||||||
|
<span class="sidebar-stat-label">Versions</span>
|
||||||
|
<span class="sidebar-stat-value">${pkg.versions.length}</span>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-stat">
|
||||||
|
<span class="sidebar-stat-label">Total Size</span>
|
||||||
|
<span class="sidebar-stat-value">${this.formatBytes(pkg.storageBytes)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-section">
|
||||||
|
<div class="sidebar-title">Info</div>
|
||||||
|
<div class="sidebar-stat">
|
||||||
|
<span class="sidebar-stat-label">Protocol</span>
|
||||||
|
<span class="sidebar-stat-value">${pkg.protocol.toUpperCase()}</span>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-stat">
|
||||||
|
<span class="sidebar-stat-label">Latest</span>
|
||||||
|
<span class="sidebar-stat-value">${pkg.latestVersion || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-stat">
|
||||||
|
<span class="sidebar-stat-label">Created</span>
|
||||||
|
<span class="sidebar-stat-value">${this.formatDate(pkg.createdAt)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-stat">
|
||||||
|
<span class="sidebar-stat-label">Updated</span>
|
||||||
|
<span class="sidebar-stat-value">${this.formatDate(pkg.updatedAt)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatNumber(n: number): string {
|
||||||
|
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
||||||
|
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
|
||||||
|
return n.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatBytes(bytes: number): string {
|
||||||
|
if (bytes === 0) return '0 B';
|
||||||
|
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||||
|
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatDate(dateStr: string): string {
|
||||||
|
if (!dateStr) return '';
|
||||||
|
try {
|
||||||
|
return new Date(dateStr).toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return dateStr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private emitEvent(name: string, detail: Record<string, unknown>) {
|
||||||
|
this.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true }));
|
||||||
|
}
|
||||||
|
}
|
||||||
427
ts_web/elements/sg-packages-list-view.ts
Normal file
427
ts_web/elements/sg-packages-list-view.ts
Normal file
@@ -0,0 +1,427 @@
|
|||||||
|
import {
|
||||||
|
DeesElement,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
css,
|
||||||
|
cssManager,
|
||||||
|
property,
|
||||||
|
type TemplateResult,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
import type { ISgPackage, TSgProtocol } from '../interfaces.js';
|
||||||
|
import './sg-protocol-badge.js';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'sg-packages-list-view': SgPackagesListView;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ALL_PROTOCOLS: TSgProtocol[] = ['npm', 'oci', 'maven', 'cargo', 'composer', 'pypi', 'rubygems'];
|
||||||
|
|
||||||
|
@customElement('sg-packages-list-view')
|
||||||
|
export class SgPackagesListView extends DeesElement {
|
||||||
|
public static demo = () => html`
|
||||||
|
<div style="padding: 24px; max-width: 1200px; background: #09090b;">
|
||||||
|
<sg-packages-list-view
|
||||||
|
.packages=${[
|
||||||
|
{ id: '1', name: '@myorg/web-framework', protocol: 'npm', organizationId: 'org1', repositoryId: 'r1', latestVersion: '3.2.1', isPrivate: false, downloadCount: 12400, updatedAt: '2026-03-19T10:30:00Z', description: 'Modern web framework for building full-stack apps' },
|
||||||
|
{ id: '2', name: 'myorg/api-gateway', protocol: 'oci', organizationId: 'org1', repositoryId: 'r2', latestVersion: 'v1.8.0', isPrivate: true, downloadCount: 890, updatedAt: '2026-03-18T14:20:00Z', description: 'API gateway container image' },
|
||||||
|
{ id: '3', name: 'data-utils', protocol: 'pypi', organizationId: 'org2', repositoryId: 'r3', latestVersion: '0.9.4', isPrivate: false, downloadCount: 3200, updatedAt: '2026-03-17T08:15:00Z', description: 'Data utilities for Python' },
|
||||||
|
{ id: '4', name: 'myorg/auth-service', protocol: 'oci', organizationId: 'org1', repositoryId: 'r2', latestVersion: 'v2.1.0', isPrivate: false, downloadCount: 4500, updatedAt: '2026-03-16T12:00:00Z' },
|
||||||
|
{ id: '5', name: '@myorg/config-tools', protocol: 'npm', organizationId: 'org1', repositoryId: 'r1', latestVersion: '1.1.3', isPrivate: false, downloadCount: 780, updatedAt: '2026-03-14T09:30:00Z', description: 'Configuration management utilities' },
|
||||||
|
]}
|
||||||
|
.total=${42}
|
||||||
|
.protocols=${['npm', 'oci', 'pypi']}
|
||||||
|
.query=${''}
|
||||||
|
></sg-packages-list-view>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
public static demoGroups = ['Packages'];
|
||||||
|
|
||||||
|
@property({ type: Array })
|
||||||
|
public accessor packages: ISgPackage[] = [];
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
public accessor total: number = 0;
|
||||||
|
|
||||||
|
@property({ type: Array })
|
||||||
|
public accessor protocols: string[] = [];
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
public accessor query: string = '';
|
||||||
|
|
||||||
|
private activeFilter: string = '';
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Search bar */
|
||||||
|
.search-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 0;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#ddd', '#333')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px 14px;
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#0a0a0a')};
|
||||||
|
border: none;
|
||||||
|
font-size: 14px;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
outline: none;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input::placeholder {
|
||||||
|
color: ${cssManager.bdTheme('#aaa', '#555')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
background: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
border: none;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-btn:hover {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Protocol filters */
|
||||||
|
.filters {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-btn {
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 150ms ease;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-btn:hover {
|
||||||
|
border-color: ${cssManager.bdTheme('#999', '#666')};
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-btn.active {
|
||||||
|
background: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
color: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
border-color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Package list */
|
||||||
|
.package-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
border-bottom: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 100ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-row:hover {
|
||||||
|
background: ${cssManager.bdTheme('#fafafa', '#1a1a1a')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-name-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.private-badge {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 1px 5px;
|
||||||
|
background: rgba(239, 68, 68, 0.15);
|
||||||
|
color: #ef4444;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-description {
|
||||||
|
font-size: 13px;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#aaa')};
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-tag {
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
|
background: ${cssManager.bdTheme('#f5f5f5', '#1a1a1a')};
|
||||||
|
padding: 2px 8px;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-count {
|
||||||
|
font-size: 12px;
|
||||||
|
color: ${cssManager.bdTheme('#888', '#777')};
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.updated-at {
|
||||||
|
font-size: 12px;
|
||||||
|
color: ${cssManager.bdTheme('#aaa', '#666')};
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pagination */
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-info {
|
||||||
|
font-size: 13px;
|
||||||
|
color: ${cssManager.bdTheme('#888', '#777')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-btn {
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
font-size: 13px;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-btn:hover {
|
||||||
|
background: ${cssManager.bdTheme('#f5f5f5', '#1a1a1a')};
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-btn:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 48px 32px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: ${cssManager.bdTheme('#888', '#777')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
public render(): TemplateResult {
|
||||||
|
const availableProtocols = this.protocols.length > 0 ? this.protocols : ALL_PROTOCOLS;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="container">
|
||||||
|
<div class="page-title">Packages</div>
|
||||||
|
|
||||||
|
<div class="search-bar">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="search-input"
|
||||||
|
placeholder="Search packages..."
|
||||||
|
.value=${this.query}
|
||||||
|
@keydown=${(e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter') this.handleSearch();
|
||||||
|
}}
|
||||||
|
@input=${(e: InputEvent) => {
|
||||||
|
this.query = (e.target as HTMLInputElement).value;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button class="search-btn" @click=${this.handleSearch}>Search</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filters">
|
||||||
|
<button
|
||||||
|
class="filter-btn ${this.activeFilter === '' ? 'active' : ''}"
|
||||||
|
@click=${() => this.handleFilter('')}
|
||||||
|
>All</button>
|
||||||
|
${availableProtocols.map(
|
||||||
|
(proto) => html`
|
||||||
|
<button
|
||||||
|
class="filter-btn ${this.activeFilter === proto ? 'active' : ''}"
|
||||||
|
@click=${() => this.handleFilter(proto)}
|
||||||
|
>${proto}</button>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${this.packages.length > 0
|
||||||
|
? html`
|
||||||
|
<div class="package-list">
|
||||||
|
${this.packages.map(
|
||||||
|
(pkg) => html`
|
||||||
|
<div class="package-row" @click=${() => this.handleSelect(pkg.id)}>
|
||||||
|
<div class="package-info">
|
||||||
|
<div class="package-name-row">
|
||||||
|
<sg-protocol-badge .protocol=${pkg.protocol}></sg-protocol-badge>
|
||||||
|
<span class="package-name">${pkg.name}</span>
|
||||||
|
${pkg.isPrivate ? html`<span class="private-badge">Private</span>` : ''}
|
||||||
|
</div>
|
||||||
|
${pkg.description
|
||||||
|
? html`<div class="package-description">${pkg.description}</div>`
|
||||||
|
: ''}
|
||||||
|
</div>
|
||||||
|
<div class="package-right">
|
||||||
|
${pkg.latestVersion ? html`<span class="version-tag">${pkg.latestVersion}</span>` : ''}
|
||||||
|
<span class="download-count">${this.formatNumber(pkg.downloadCount)}</span>
|
||||||
|
<span class="updated-at">${this.formatDate(pkg.updatedAt)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pagination">
|
||||||
|
<div class="pagination-info">
|
||||||
|
Showing ${this.packages.length} of ${this.total} packages
|
||||||
|
</div>
|
||||||
|
<div class="pagination-buttons">
|
||||||
|
<button class="page-btn" @click=${() => this.handlePage(-1)}>Previous</button>
|
||||||
|
<button class="page-btn" @click=${() => this.handlePage(1)}>Next</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: html`<div class="empty-state">No packages found${this.query ? ` for "${this.query}"` : ''}</div>`}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatNumber(n: number): string {
|
||||||
|
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
||||||
|
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
|
||||||
|
return n.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatDate(dateStr: string): string {
|
||||||
|
if (!dateStr) return '';
|
||||||
|
try {
|
||||||
|
return new Date(dateStr).toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
||||||
|
} catch {
|
||||||
|
return dateStr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleSearch() {
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('search', {
|
||||||
|
detail: { query: this.query },
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleFilter(protocol: string) {
|
||||||
|
this.activeFilter = protocol;
|
||||||
|
this.requestUpdate();
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('filter', {
|
||||||
|
detail: { protocol },
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleSelect(packageId: string) {
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('select', {
|
||||||
|
detail: { packageId },
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handlePage(direction: number) {
|
||||||
|
const currentOffset = this.packages.length;
|
||||||
|
const offset = direction > 0 ? currentOffset : Math.max(0, currentOffset - this.packages.length * 2);
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('page', {
|
||||||
|
detail: { offset },
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
74
ts_web/elements/sg-protocol-badge.ts
Normal file
74
ts_web/elements/sg-protocol-badge.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import {
|
||||||
|
DeesElement,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
property,
|
||||||
|
css,
|
||||||
|
cssManager,
|
||||||
|
type TemplateResult,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
import type { TSgProtocol } from '../interfaces.js';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'sg-protocol-badge': SgProtocolBadge;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const protocolColors: Record<string, string> = {
|
||||||
|
npm: '#cb3837',
|
||||||
|
oci: '#2496ed',
|
||||||
|
maven: '#c71a36',
|
||||||
|
cargo: '#dea584',
|
||||||
|
composer: '#885630',
|
||||||
|
pypi: '#3775a9',
|
||||||
|
rubygems: '#e9573f',
|
||||||
|
};
|
||||||
|
|
||||||
|
@customElement('sg-protocol-badge')
|
||||||
|
export class SgProtocolBadge extends DeesElement {
|
||||||
|
public static demo = () => html`
|
||||||
|
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
|
||||||
|
<sg-protocol-badge .protocol=${'npm'}></sg-protocol-badge>
|
||||||
|
<sg-protocol-badge .protocol=${'oci'}></sg-protocol-badge>
|
||||||
|
<sg-protocol-badge .protocol=${'maven'}></sg-protocol-badge>
|
||||||
|
<sg-protocol-badge .protocol=${'cargo'}></sg-protocol-badge>
|
||||||
|
<sg-protocol-badge .protocol=${'pypi'}></sg-protocol-badge>
|
||||||
|
<sg-protocol-badge .protocol=${'composer'}></sg-protocol-badge>
|
||||||
|
<sg-protocol-badge .protocol=${'rubygems'}></sg-protocol-badge>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
public static demoGroups = ['Shared'];
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
accessor protocol: TSgProtocol = 'npm';
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2px 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
public render(): TemplateResult {
|
||||||
|
const color = protocolColors[this.protocol] || '#666';
|
||||||
|
return html`
|
||||||
|
<span class="badge" style="background: ${color}">
|
||||||
|
${this.protocol}
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
179
ts_web/elements/sg-public-layout.ts
Normal file
179
ts_web/elements/sg-public-layout.ts
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
import {
|
||||||
|
DeesElement,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
css,
|
||||||
|
cssManager,
|
||||||
|
property,
|
||||||
|
type TemplateResult,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'sg-public-layout': SgPublicLayout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement('sg-public-layout')
|
||||||
|
export class SgPublicLayout extends DeesElement {
|
||||||
|
public static demo = () => html`
|
||||||
|
<sg-public-layout>
|
||||||
|
<div style="padding: 48px; text-align: center; color: #666;">
|
||||||
|
Page content goes here
|
||||||
|
</div>
|
||||||
|
</sg-public-layout>
|
||||||
|
`;
|
||||||
|
|
||||||
|
public static demoGroups = ['Public'];
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: ${cssManager.bdTheme('#f4f4f5', '#09090b')};
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-bar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 56px;
|
||||||
|
padding: 0 24px;
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#09090b')};
|
||||||
|
border-bottom: 1px solid ${cssManager.bdTheme('#e5e5e5', '#222')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.foss-btn {
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#ddd', '#444')};
|
||||||
|
font-size: 12px;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 150ms ease;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.foss-btn:hover {
|
||||||
|
border-color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.sign-in-btn {
|
||||||
|
padding: 6px 16px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#ddd', '#444')};
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sign-in-btn:hover {
|
||||||
|
border-color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
flex: 1;
|
||||||
|
margin-top: 56px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusbar {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 100;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 12px;
|
||||||
|
background: ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(0 0% 6%)')};
|
||||||
|
border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(0 0% 15%)')};
|
||||||
|
font-size: 11px;
|
||||||
|
color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 60%)')};
|
||||||
|
}
|
||||||
|
.statusbar-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.statusbar-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.statusbar a {
|
||||||
|
color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 60%)')};
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.statusbar a:hover {
|
||||||
|
color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 80%)')};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
public render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<div class="top-bar">
|
||||||
|
<div class="logo">Stack.Gallery</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<a class="foss-btn" href="https://code.foss.global/stack.gallery/registry" target="_blank">View on foss.global</a>
|
||||||
|
<button class="sign-in-btn" @click=${this.handleSignIn}>Sign in</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="statusbar">
|
||||||
|
<div class="statusbar-left">
|
||||||
|
<span>Powered by Stack.Gallery</span>
|
||||||
|
</div>
|
||||||
|
<div class="statusbar-right">
|
||||||
|
<a href="https://code.foss.global/stack.gallery/registry" target="_blank">Host your own private on-prem registry</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleSignIn() {
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('sign-in', {
|
||||||
|
detail: {},
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
658
ts_web/elements/sg-public-search-view.ts
Normal file
658
ts_web/elements/sg-public-search-view.ts
Normal file
@@ -0,0 +1,658 @@
|
|||||||
|
import {
|
||||||
|
DeesElement,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
css,
|
||||||
|
cssManager,
|
||||||
|
property,
|
||||||
|
type TemplateResult,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
import type { ISgPackage, TSgProtocol } from '../interfaces.js';
|
||||||
|
import './sg-protocol-badge.js';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'sg-public-search-view': SgPublicSearchView;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ALL_PROTOCOLS: TSgProtocol[] = ['npm', 'oci', 'maven', 'cargo', 'composer', 'pypi', 'rubygems'];
|
||||||
|
|
||||||
|
@customElement('sg-public-search-view')
|
||||||
|
export class SgPublicSearchView extends DeesElement {
|
||||||
|
public static demo = () => html`
|
||||||
|
<div style="background: #09090b; min-height: 800px;">
|
||||||
|
<sg-public-search-view
|
||||||
|
.packages=${[
|
||||||
|
{ id: '1', name: '@myorg/web-framework', protocol: 'npm', organizationId: 'org1', repositoryId: 'r1', latestVersion: '3.2.1', isPrivate: false, downloadCount: 12400, updatedAt: '2026-03-19T10:30:00Z', description: 'Modern web framework for building full-stack applications with TypeScript' },
|
||||||
|
{ id: '2', name: 'myorg/api-gateway', protocol: 'oci', organizationId: 'org1', repositoryId: 'r2', latestVersion: 'v1.8.0', isPrivate: false, downloadCount: 890, updatedAt: '2026-03-18T14:20:00Z', description: 'API gateway container image with rate limiting and auth' },
|
||||||
|
{ id: '3', name: 'data-utils', protocol: 'pypi', organizationId: 'org2', repositoryId: 'r3', latestVersion: '0.9.4', isPrivate: false, downloadCount: 3200, updatedAt: '2026-03-17T08:15:00Z', description: 'Data utilities for Python with pandas integration' },
|
||||||
|
{ id: '4', name: 'myorg/auth-service', protocol: 'oci', organizationId: 'org1', repositoryId: 'r2', latestVersion: 'v2.1.0', isPrivate: false, downloadCount: 4500, updatedAt: '2026-03-16T12:00:00Z', description: 'Authentication microservice with OAuth2 and SAML support' },
|
||||||
|
{ id: '5', name: '@myorg/config-tools', protocol: 'npm', organizationId: 'org1', repositoryId: 'r1', latestVersion: '1.1.3', isPrivate: false, downloadCount: 780, updatedAt: '2026-03-14T09:30:00Z', description: 'Configuration management utilities for Node.js' },
|
||||||
|
{ id: '6', name: 'com.myorg:db-driver', protocol: 'maven', organizationId: 'org3', repositoryId: 'r4', latestVersion: '2.0.0', isPrivate: false, downloadCount: 5600, updatedAt: '2026-03-13T16:45:00Z', description: 'High-performance database driver for JVM applications' },
|
||||||
|
]}
|
||||||
|
.total=${42}
|
||||||
|
.query=${'web framework'}
|
||||||
|
.activeProtocol=${''}
|
||||||
|
></sg-public-search-view>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
public static demoGroups = ['Public'];
|
||||||
|
|
||||||
|
@property({ type: Array })
|
||||||
|
public accessor packages: ISgPackage[] = [];
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
public accessor total: number = 0;
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
public accessor query: string = '';
|
||||||
|
|
||||||
|
@property({ type: Array })
|
||||||
|
public accessor protocols: string[] = [];
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
public accessor activeProtocol: string = '';
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
public accessor loading: boolean = false;
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hero section */
|
||||||
|
.hero {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 80px 24px 64px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
font-size: 40px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-subtitle {
|
||||||
|
font-size: 16px;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
|
max-width: 600px;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-search {
|
||||||
|
display: flex;
|
||||||
|
gap: 0;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#ddd', '#333')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-search-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 14px 18px;
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#0a0a0a')};
|
||||||
|
border: none;
|
||||||
|
font-size: 16px;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
outline: none;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-search-input::placeholder {
|
||||||
|
color: ${cssManager.bdTheme('#aaa', '#555')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-search-btn {
|
||||||
|
padding: 14px 28px;
|
||||||
|
background: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
border: none;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-search-btn:hover {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.protocol-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Compact search bar (shown in results mode) */
|
||||||
|
.compact-search {
|
||||||
|
max-width: 960px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 24px 24px 0;
|
||||||
|
display: flex;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-search-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px 14px;
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#0a0a0a')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#ddd', '#333')};
|
||||||
|
border-right: none;
|
||||||
|
font-size: 14px;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
outline: none;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-search-input::placeholder {
|
||||||
|
color: ${cssManager.bdTheme('#aaa', '#555')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-search-input:focus {
|
||||||
|
border-color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-search-btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
background: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
border: none;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-search-btn:hover {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Results section */
|
||||||
|
.results {
|
||||||
|
max-width: 960px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 24px 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-count {
|
||||||
|
font-size: 13px;
|
||||||
|
color: ${cssManager.bdTheme('#888', '#777')};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Protocol filter tabs */
|
||||||
|
.filters {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-btn {
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 150ms ease;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-btn:hover {
|
||||||
|
border-color: ${cssManager.bdTheme('#999', '#666')};
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-btn.active {
|
||||||
|
background: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
color: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
border-color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Package cards */
|
||||||
|
.package-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px 18px;
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
border-bottom: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 100ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-card:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-card:hover {
|
||||||
|
background: ${cssManager.bdTheme('#fafafa', '#1a1a1a')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-name-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-description {
|
||||||
|
font-size: 13px;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#aaa')};
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-item {
|
||||||
|
font-size: 11px;
|
||||||
|
color: ${cssManager.bdTheme('#aaa', '#666')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-separator {
|
||||||
|
font-size: 11px;
|
||||||
|
color: ${cssManager.bdTheme('#ddd', '#444')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-tag {
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
|
background: ${cssManager.bdTheme('#f5f5f5', '#1a1a1a')};
|
||||||
|
padding: 2px 8px;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-count {
|
||||||
|
font-size: 12px;
|
||||||
|
color: ${cssManager.bdTheme('#888', '#777')};
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pagination */
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-info {
|
||||||
|
font-size: 13px;
|
||||||
|
color: ${cssManager.bdTheme('#888', '#777')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-btn {
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
font-size: 13px;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-btn:hover {
|
||||||
|
background: ${cssManager.bdTheme('#f5f5f5', '#1a1a1a')};
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-btn:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading state */
|
||||||
|
.loading-overlay {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 64px 24px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: ${cssManager.bdTheme('#888', '#777')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
border-top-color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.7s linear infinite;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty state */
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 48px 32px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: ${cssManager.bdTheme('#888', '#777')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hero empty placeholder */
|
||||||
|
.hero-placeholder {
|
||||||
|
margin-top: 32px;
|
||||||
|
padding: 32px;
|
||||||
|
border: 1px dashed ${cssManager.bdTheme('#ddd', '#333')};
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.hero-placeholder-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
.hero-placeholder-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.hero-placeholder-text {
|
||||||
|
font-size: 13px;
|
||||||
|
color: ${cssManager.bdTheme('#999', '#666')};
|
||||||
|
line-height: 1.5;
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
public render(): TemplateResult {
|
||||||
|
const showHero = this.packages.length === 0 && !this.query && !this.loading;
|
||||||
|
const showResults = this.packages.length > 0 || this.query || this.loading;
|
||||||
|
const availableProtocols = this.protocols.length > 0 ? this.protocols : ALL_PROTOCOLS;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
${showHero ? this.renderHero() : ''}
|
||||||
|
${showResults ? this.renderResults(availableProtocols) : ''}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderHero(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<div class="hero">
|
||||||
|
<div class="hero-title">Stack.Gallery Registry</div>
|
||||||
|
<div class="hero-subtitle">
|
||||||
|
Browse and discover packages across npm, OCI, Maven, Cargo, PyPI, Composer, and RubyGems
|
||||||
|
</div>
|
||||||
|
<div class="hero-search">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="hero-search-input"
|
||||||
|
id="hero-search-input"
|
||||||
|
placeholder="Search packages..."
|
||||||
|
@keydown=${(e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
this.query = (e.target as HTMLInputElement).value;
|
||||||
|
this.handleSearch();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button class="hero-search-btn" @click=${() => {
|
||||||
|
const input = this.shadowRoot?.getElementById('hero-search-input') as HTMLInputElement;
|
||||||
|
if (input) {
|
||||||
|
this.query = input.value;
|
||||||
|
this.handleSearch();
|
||||||
|
}
|
||||||
|
}}>Search</button>
|
||||||
|
</div>
|
||||||
|
<div class="protocol-row">
|
||||||
|
${ALL_PROTOCOLS.map(
|
||||||
|
(proto) => html`<sg-protocol-badge .protocol=${proto}></sg-protocol-badge>`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
${this.total === 0 ? html`
|
||||||
|
<div class="hero-placeholder">
|
||||||
|
<div class="hero-placeholder-icon">\u{1F4E6}</div>
|
||||||
|
<div class="hero-placeholder-title">No public packages yet</div>
|
||||||
|
<div class="hero-placeholder-text">
|
||||||
|
This registry is ready to host packages. Sign in to create an organization and start publishing.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderCompactSearch(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<div class="compact-search">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="compact-search-input"
|
||||||
|
id="compact-search-input"
|
||||||
|
placeholder="Search packages..."
|
||||||
|
.value=${this.query}
|
||||||
|
@keydown=${(e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
this.query = (e.target as HTMLInputElement).value;
|
||||||
|
this.handleSearch();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button class="compact-search-btn" @click=${() => {
|
||||||
|
const input = this.shadowRoot?.getElementById('compact-search-input') as HTMLInputElement;
|
||||||
|
if (input) {
|
||||||
|
this.query = input.value;
|
||||||
|
this.handleSearch();
|
||||||
|
}
|
||||||
|
}}>Search</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderResults(availableProtocols: string[]): TemplateResult {
|
||||||
|
return html`
|
||||||
|
${this.renderCompactSearch()}
|
||||||
|
<div class="results">
|
||||||
|
<div class="filters">
|
||||||
|
<button
|
||||||
|
class="filter-btn ${this.activeProtocol === '' ? 'active' : ''}"
|
||||||
|
@click=${() => this.handleFilter('')}
|
||||||
|
>All</button>
|
||||||
|
${availableProtocols.map(
|
||||||
|
(proto) => html`
|
||||||
|
<button
|
||||||
|
class="filter-btn ${this.activeProtocol === proto ? 'active' : ''}"
|
||||||
|
@click=${() => this.handleFilter(proto)}
|
||||||
|
>${proto}</button>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="results-header">
|
||||||
|
<span class="results-count">
|
||||||
|
${this.total} package${this.total !== 1 ? 's' : ''} found${this.query ? ` for "${this.query}"` : ''}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${this.loading
|
||||||
|
? html`
|
||||||
|
<div class="loading-overlay">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
Searching packages...
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: this.packages.length > 0
|
||||||
|
? html`
|
||||||
|
<div class="package-grid">
|
||||||
|
${this.packages.map(
|
||||||
|
(pkg) => html`
|
||||||
|
<div class="package-card" @click=${() => this.handleSelect(pkg.id)}>
|
||||||
|
<div class="package-info">
|
||||||
|
<div class="package-name-row">
|
||||||
|
<sg-protocol-badge .protocol=${pkg.protocol}></sg-protocol-badge>
|
||||||
|
<span class="package-name">${pkg.name}</span>
|
||||||
|
</div>
|
||||||
|
${pkg.description
|
||||||
|
? html`<div class="package-description">${pkg.description}</div>`
|
||||||
|
: ''}
|
||||||
|
<div class="package-meta">
|
||||||
|
<span class="meta-item">${this.formatNumber(pkg.downloadCount)} downloads</span>
|
||||||
|
<span class="meta-separator">·</span>
|
||||||
|
<span class="meta-item">${this.formatDate(pkg.updatedAt)}</span>
|
||||||
|
<span class="meta-separator">·</span>
|
||||||
|
<span class="meta-item">${pkg.organizationId}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="package-right">
|
||||||
|
${pkg.latestVersion
|
||||||
|
? html`<span class="version-tag">${pkg.latestVersion}</span>`
|
||||||
|
: ''}
|
||||||
|
<span class="download-count">${this.formatNumber(pkg.downloadCount)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pagination">
|
||||||
|
<div class="pagination-info">
|
||||||
|
Showing ${this.packages.length} of ${this.total} packages
|
||||||
|
</div>
|
||||||
|
<div class="pagination-buttons">
|
||||||
|
<button class="page-btn" @click=${() => this.handlePage(-1)}>Previous</button>
|
||||||
|
<button class="page-btn" @click=${() => this.handlePage(1)}>Next</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: html`<div class="empty-state">No packages found${this.query ? ` for "${this.query}"` : ''}</div>`
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatNumber(n: number): string {
|
||||||
|
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
||||||
|
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
|
||||||
|
return n.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatDate(dateStr: string): string {
|
||||||
|
if (!dateStr) return '';
|
||||||
|
try {
|
||||||
|
return new Date(dateStr).toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
||||||
|
} catch {
|
||||||
|
return dateStr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleSearch() {
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('search', {
|
||||||
|
detail: { query: this.query },
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleFilter(protocol: string) {
|
||||||
|
this.activeProtocol = protocol;
|
||||||
|
this.requestUpdate();
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('filter', {
|
||||||
|
detail: { protocol },
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleSelect(packageId: string) {
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('select', {
|
||||||
|
detail: { packageId },
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handlePage(direction: number) {
|
||||||
|
const currentOffset = this.packages.length;
|
||||||
|
const offset = direction > 0 ? currentOffset : Math.max(0, currentOffset - this.packages.length * 2);
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('page', {
|
||||||
|
detail: { offset },
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
327
ts_web/elements/sg-repository-detail-view.ts
Normal file
327
ts_web/elements/sg-repository-detail-view.ts
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
import {
|
||||||
|
DeesElement,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
css,
|
||||||
|
cssManager,
|
||||||
|
property,
|
||||||
|
type TemplateResult,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
import type { ISgRepository, ISgPackage } from '../interfaces.js';
|
||||||
|
import './sg-protocol-badge.js';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'sg-repository-detail-view': SgRepositoryDetailView;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement('sg-repository-detail-view')
|
||||||
|
export class SgRepositoryDetailView extends DeesElement {
|
||||||
|
public static demo = () => html`
|
||||||
|
<div style="padding: 24px; max-width: 1200px; background: #09090b;">
|
||||||
|
<sg-repository-detail-view
|
||||||
|
.repository=${{
|
||||||
|
id: 'r1',
|
||||||
|
organizationId: 'org1',
|
||||||
|
name: 'npm-packages',
|
||||||
|
description: 'Organization NPM package repository for internal and public libraries.',
|
||||||
|
protocol: 'npm',
|
||||||
|
visibility: 'public',
|
||||||
|
isPublic: true,
|
||||||
|
packageCount: 5,
|
||||||
|
createdAt: '2025-06-15',
|
||||||
|
}}
|
||||||
|
.packages=${[
|
||||||
|
{ id: 'p1', name: '@myorg/web-framework', protocol: 'npm', organizationId: 'org1', repositoryId: 'r1', latestVersion: '3.2.1', isPrivate: false, downloadCount: 12400, updatedAt: '2026-03-19T10:30:00Z', description: 'Modern web framework' },
|
||||||
|
{ id: 'p2', name: '@myorg/auth-client', protocol: 'npm', organizationId: 'org1', repositoryId: 'r1', latestVersion: '1.0.8', isPrivate: false, downloadCount: 5200, updatedAt: '2026-03-18T14:00:00Z', description: 'Authentication client library' },
|
||||||
|
{ id: 'p3', name: '@myorg/data-utils', protocol: 'npm', organizationId: 'org1', repositoryId: 'r1', latestVersion: '2.4.0', isPrivate: true, downloadCount: 890, updatedAt: '2026-03-15T08:45:00Z' },
|
||||||
|
]}
|
||||||
|
></sg-repository-detail-view>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
public static demoGroups = ['Repositories'];
|
||||||
|
|
||||||
|
@property({ type: Object })
|
||||||
|
public accessor repository: ISgRepository = {
|
||||||
|
id: '',
|
||||||
|
organizationId: '',
|
||||||
|
name: '',
|
||||||
|
protocol: 'npm',
|
||||||
|
visibility: 'public',
|
||||||
|
isPublic: true,
|
||||||
|
packageCount: 0,
|
||||||
|
createdAt: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
@property({ type: Array })
|
||||||
|
public accessor packages: ISgPackage[] = [];
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
font-size: 13px;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn:hover {
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Repo header */
|
||||||
|
.repo-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-name {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-description {
|
||||||
|
font-size: 14px;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#aaa')};
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-meta {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: ${cssManager.bdTheme('#888', '#777')};
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visibility-tag {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
padding: 2px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visibility-tag.public {
|
||||||
|
background: rgba(34, 197, 94, 0.15);
|
||||||
|
color: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visibility-tag.private {
|
||||||
|
background: rgba(239, 68, 68, 0.15);
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visibility-tag.internal {
|
||||||
|
background: rgba(59, 130, 246, 0.15);
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Package list */
|
||||||
|
.packages-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
border-bottom: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 100ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-row:hover {
|
||||||
|
background: ${cssManager.bdTheme('#fafafa', '#1a1a1a')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-description {
|
||||||
|
font-size: 13px;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#aaa')};
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-tag {
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
|
background: ${cssManager.bdTheme('#f5f5f5', '#1a1a1a')};
|
||||||
|
padding: 2px 8px;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-count {
|
||||||
|
font-size: 12px;
|
||||||
|
color: ${cssManager.bdTheme('#888', '#777')};
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.private-badge {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 1px 5px;
|
||||||
|
background: rgba(239, 68, 68, 0.15);
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 48px 32px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: ${cssManager.bdTheme('#888', '#777')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
public render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<div class="container">
|
||||||
|
<button class="back-btn" @click=${() => this.emitEvent('back', {})}>
|
||||||
|
\u2190 Back to organization
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="repo-header">
|
||||||
|
<div class="repo-title-row">
|
||||||
|
<sg-protocol-badge .protocol=${this.repository.protocol}></sg-protocol-badge>
|
||||||
|
<span class="repo-name">${this.repository.name}</span>
|
||||||
|
<span class="visibility-tag ${this.repository.visibility}">${this.repository.visibility}</span>
|
||||||
|
</div>
|
||||||
|
${this.repository.description
|
||||||
|
? html`<div class="repo-description">${this.repository.description}</div>`
|
||||||
|
: ''}
|
||||||
|
<div class="repo-meta">
|
||||||
|
<span>${this.repository.packageCount} package${this.repository.packageCount !== 1 ? 's' : ''}</span>
|
||||||
|
<span>Protocol: ${this.repository.protocol.toUpperCase()}</span>
|
||||||
|
<span>Created: ${this.formatDate(this.repository.createdAt)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="packages-section">
|
||||||
|
<div class="section-title">Packages</div>
|
||||||
|
${this.packages.length > 0
|
||||||
|
? html`
|
||||||
|
<div class="package-list">
|
||||||
|
${this.packages.map(
|
||||||
|
(pkg) => html`
|
||||||
|
<div class="package-row" @click=${() => this.emitEvent('select-package', { packageId: pkg.id })}>
|
||||||
|
<div class="package-info">
|
||||||
|
<div class="package-name">${pkg.name}</div>
|
||||||
|
${pkg.description
|
||||||
|
? html`<div class="package-description">${pkg.description}</div>`
|
||||||
|
: ''}
|
||||||
|
</div>
|
||||||
|
<div class="package-right">
|
||||||
|
${pkg.isPrivate ? html`<span class="private-badge">Private</span>` : ''}
|
||||||
|
${pkg.latestVersion ? html`<span class="version-tag">${pkg.latestVersion}</span>` : ''}
|
||||||
|
<span class="download-count">${this.formatNumber(pkg.downloadCount)} pulls</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: html`<div class="empty-state">No packages published to this repository yet.</div>`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatNumber(n: number): string {
|
||||||
|
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
||||||
|
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
|
||||||
|
return n.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatDate(dateStr: string): string {
|
||||||
|
if (!dateStr) return '';
|
||||||
|
try {
|
||||||
|
return new Date(dateStr).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
|
||||||
|
} catch {
|
||||||
|
return dateStr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private emitEvent(name: string, detail: Record<string, unknown>) {
|
||||||
|
this.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true }));
|
||||||
|
}
|
||||||
|
}
|
||||||
470
ts_web/elements/sg-settings-view.ts
Normal file
470
ts_web/elements/sg-settings-view.ts
Normal file
@@ -0,0 +1,470 @@
|
|||||||
|
import {
|
||||||
|
DeesElement,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
css,
|
||||||
|
cssManager,
|
||||||
|
property,
|
||||||
|
type TemplateResult,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
import type { ISgUser, ISgSession } from '../interfaces.js';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'sg-settings-view': SgSettingsView;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement('sg-settings-view')
|
||||||
|
export class SgSettingsView extends DeesElement {
|
||||||
|
public static demo = () => html`
|
||||||
|
<div style="padding: 24px; max-width: 900px; background: #09090b;">
|
||||||
|
<sg-settings-view
|
||||||
|
.user=${{
|
||||||
|
id: 'u1',
|
||||||
|
email: 'admin@stack.gallery',
|
||||||
|
username: 'admin',
|
||||||
|
displayName: 'Admin User',
|
||||||
|
avatarUrl: '',
|
||||||
|
isSystemAdmin: true,
|
||||||
|
}}
|
||||||
|
.sessions=${[
|
||||||
|
{ id: 's1', userAgent: 'Mozilla/5.0 (X11; Linux x86_64) Chrome/120.0', ipAddress: '192.168.1.100', isValid: true, lastActivityAt: '2026-03-20T10:30:00Z', createdAt: '2026-03-18T08:00:00Z' },
|
||||||
|
{ id: 's2', userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X) Safari/17.0', ipAddress: '10.0.0.42', isValid: true, lastActivityAt: '2026-03-19T15:45:00Z', createdAt: '2026-03-15T14:00:00Z' },
|
||||||
|
{ id: 's3', userAgent: 'curl/8.4.0', ipAddress: '203.0.113.50', isValid: false, lastActivityAt: '2026-03-10T22:00:00Z', createdAt: '2026-03-10T20:00:00Z' },
|
||||||
|
]}
|
||||||
|
></sg-settings-view>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
public static demoGroups = ['Auth'];
|
||||||
|
|
||||||
|
@property({ type: Object })
|
||||||
|
public accessor user: ISgUser = {
|
||||||
|
id: '',
|
||||||
|
email: '',
|
||||||
|
username: '',
|
||||||
|
displayName: '',
|
||||||
|
isSystemAdmin: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
@property({ type: Array })
|
||||||
|
public accessor sessions: ISgSession[] = [];
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section */
|
||||||
|
.section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-box {
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
padding: 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-subtitle {
|
||||||
|
font-size: 13px;
|
||||||
|
color: ${cssManager.bdTheme('#888', '#777')};
|
||||||
|
margin-top: -8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form elements */
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#ddd')};
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#0a0a0a')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#ddd', '#333')};
|
||||||
|
font-size: 14px;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
outline: none;
|
||||||
|
font-family: inherit;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:focus {
|
||||||
|
border-color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: ${cssManager.bdTheme('#aaa', '#666')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn {
|
||||||
|
align-self: flex-start;
|
||||||
|
padding: 8px 20px;
|
||||||
|
background: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
border: none;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn:hover {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Admin badge */
|
||||||
|
.admin-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
padding: 2px 8px;
|
||||||
|
background: rgba(59, 130, 246, 0.15);
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sessions */
|
||||||
|
.session-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
border-bottom: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-agent {
|
||||||
|
font-size: 13px;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-meta {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: ${cssManager.bdTheme('#888', '#777')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-status {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 1px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-status.active {
|
||||||
|
background: rgba(34, 197, 94, 0.15);
|
||||||
|
color: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-status.expired {
|
||||||
|
background: rgba(239, 68, 68, 0.15);
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-actions {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.revoke-session-btn {
|
||||||
|
padding: 4px 12px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#ddd', '#333')};
|
||||||
|
font-size: 12px;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.revoke-session-btn:hover {
|
||||||
|
border-color: #ef4444;
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Danger zone */
|
||||||
|
.danger-section {
|
||||||
|
border-color: rgba(239, 68, 68, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-title {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-text {
|
||||||
|
font-size: 13px;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#aaa')};
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-btn {
|
||||||
|
align-self: flex-start;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #ef4444;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ef4444;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-btn:hover {
|
||||||
|
background: #ef4444;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
public render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<div class="container">
|
||||||
|
<div class="page-title">Settings</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">
|
||||||
|
Profile
|
||||||
|
${this.user.isSystemAdmin ? html`<span class="admin-badge">Admin</span>` : ''}
|
||||||
|
</div>
|
||||||
|
<div class="section-box">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Email</label>
|
||||||
|
<input type="email" class="form-input" .value=${this.user.email} disabled>
|
||||||
|
<span class="form-hint">Email cannot be changed</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Username</label>
|
||||||
|
<input type="text" class="form-input" .value=${this.user.username} disabled>
|
||||||
|
<span class="form-hint">Username cannot be changed</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Display Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="settings-displayname"
|
||||||
|
class="form-input"
|
||||||
|
.value=${this.user.displayName}
|
||||||
|
placeholder="Your display name"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Avatar URL</label>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
id="settings-avatar"
|
||||||
|
class="form-input"
|
||||||
|
.value=${this.user.avatarUrl || ''}
|
||||||
|
placeholder="https://..."
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<button class="save-btn" @click=${this.handleSaveProfile}>Save Profile</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">Change Password</div>
|
||||||
|
<div class="section-box">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Current Password</label>
|
||||||
|
<input type="password" id="settings-current-pw" class="form-input" placeholder="Current password">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">New Password</label>
|
||||||
|
<input type="password" id="settings-new-pw" class="form-input" placeholder="New password">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Confirm New Password</label>
|
||||||
|
<input type="password" id="settings-confirm-pw" class="form-input" placeholder="Confirm new password">
|
||||||
|
</div>
|
||||||
|
<button class="save-btn" @click=${this.handleChangePassword}>Update Password</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">Active Sessions</div>
|
||||||
|
${this.sessions.length > 0
|
||||||
|
? html`
|
||||||
|
<div class="session-list">
|
||||||
|
${this.sessions.map(
|
||||||
|
(session) => html`
|
||||||
|
<div class="session-row">
|
||||||
|
<div class="session-info">
|
||||||
|
<div class="session-agent">${this.parseUserAgent(session.userAgent || 'Unknown client')}</div>
|
||||||
|
<div class="session-meta">
|
||||||
|
<span>${session.ipAddress || 'unknown'}</span>
|
||||||
|
<span>Active ${this.formatDate(session.lastActivityAt || '')}</span>
|
||||||
|
<span class="session-status ${session.isValid ? 'active' : 'expired'}">
|
||||||
|
${session.isValid ? 'Active' : 'Expired'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="session-actions">
|
||||||
|
${session.isValid
|
||||||
|
? html`
|
||||||
|
<button
|
||||||
|
class="revoke-session-btn"
|
||||||
|
@click=${() => this.handleRevokeSession(session.id)}
|
||||||
|
>Revoke</button>
|
||||||
|
`
|
||||||
|
: ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: html`<div class="section-box">No active sessions</div>`}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title danger-title">Danger Zone</div>
|
||||||
|
<div class="section-box danger-section">
|
||||||
|
<div class="danger-text">
|
||||||
|
Permanently delete your account and all associated data. This action cannot be undone.
|
||||||
|
All your tokens will be revoked and organization memberships removed.
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Confirm with password</label>
|
||||||
|
<input type="password" id="settings-delete-pw" class="form-input" placeholder="Enter your password">
|
||||||
|
</div>
|
||||||
|
<button class="danger-btn" @click=${this.handleDeleteAccount}>Delete My Account</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseUserAgent(ua: string): string {
|
||||||
|
if (!ua) return 'Unknown client';
|
||||||
|
if (ua.length > 80) return ua.substring(0, 77) + '...';
|
||||||
|
return ua;
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatDate(dateStr: string): string {
|
||||||
|
if (!dateStr) return '';
|
||||||
|
try {
|
||||||
|
return new Date(dateStr).toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return dateStr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleSaveProfile() {
|
||||||
|
const displayName = (this.shadowRoot?.getElementById('settings-displayname') as HTMLInputElement)?.value || '';
|
||||||
|
const avatarUrl = (this.shadowRoot?.getElementById('settings-avatar') as HTMLInputElement)?.value || '';
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('save-profile', {
|
||||||
|
detail: { displayName, avatarUrl },
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleChangePassword() {
|
||||||
|
const currentPassword = (this.shadowRoot?.getElementById('settings-current-pw') as HTMLInputElement)?.value || '';
|
||||||
|
const newPassword = (this.shadowRoot?.getElementById('settings-new-pw') as HTMLInputElement)?.value || '';
|
||||||
|
const confirmPassword = (this.shadowRoot?.getElementById('settings-confirm-pw') as HTMLInputElement)?.value || '';
|
||||||
|
if (!currentPassword || !newPassword) return;
|
||||||
|
if (newPassword !== confirmPassword) return;
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('change-password', {
|
||||||
|
detail: { currentPassword, newPassword },
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleRevokeSession(sessionId: string) {
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('revoke-session', {
|
||||||
|
detail: { sessionId },
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleDeleteAccount() {
|
||||||
|
const password = (this.shadowRoot?.getElementById('settings-delete-pw') as HTMLInputElement)?.value || '';
|
||||||
|
if (!password) return;
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('delete-account', {
|
||||||
|
detail: { password },
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
96
ts_web/elements/sg-stat-card.ts
Normal file
96
ts_web/elements/sg-stat-card.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import {
|
||||||
|
DeesElement,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
property,
|
||||||
|
css,
|
||||||
|
cssManager,
|
||||||
|
type TemplateResult,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
import type { ISgStat } from '../interfaces.js';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'sg-stat-card': SgStatCard;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement('sg-stat-card')
|
||||||
|
export class SgStatCard extends DeesElement {
|
||||||
|
public static demo = () => html`<sg-stat-card
|
||||||
|
.title=${'Total Packages'}
|
||||||
|
.value=${1234}
|
||||||
|
.icon=${'lucide:package'}
|
||||||
|
.trend=${'up'}
|
||||||
|
.trendValue=${'+12%'}
|
||||||
|
></sg-stat-card>`;
|
||||||
|
public static demoGroups = ['Dashboard'];
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
accessor title: string = '';
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
accessor value: string | number = 0;
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
accessor icon: string = '';
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
accessor trend: 'up' | 'down' | 'neutral' = 'neutral';
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
accessor trendValue: string = '';
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#1a1a1a')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.card-title {
|
||||||
|
font-size: 13px;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
.card-value {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
}
|
||||||
|
.card-trend {
|
||||||
|
font-size: 13px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.card-trend.up { color: #22c55e; }
|
||||||
|
.card-trend.down { color: #ef4444; }
|
||||||
|
.card-trend.neutral { color: ${cssManager.bdTheme('#999', '#666')}; }
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
public render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title">${this.title}</div>
|
||||||
|
<div class="card-value">${this.value}</div>
|
||||||
|
${this.trendValue ? html`
|
||||||
|
<div class="card-trend ${this.trend}">
|
||||||
|
${this.trend === 'up' ? '\u2191' : this.trend === 'down' ? '\u2193' : '\u2022'}
|
||||||
|
${this.trendValue}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
495
ts_web/elements/sg-tokens-view.ts
Normal file
495
ts_web/elements/sg-tokens-view.ts
Normal file
@@ -0,0 +1,495 @@
|
|||||||
|
import {
|
||||||
|
DeesElement,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
css,
|
||||||
|
cssManager,
|
||||||
|
property,
|
||||||
|
type TemplateResult,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
import type { ISgToken, ISgOrganization, TSgProtocol } from '../interfaces.js';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'sg-tokens-view': SgTokensView;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ALL_PROTOCOLS: TSgProtocol[] = ['npm', 'oci', 'maven', 'cargo', 'composer', 'pypi', 'rubygems'];
|
||||||
|
|
||||||
|
@customElement('sg-tokens-view')
|
||||||
|
export class SgTokensView extends DeesElement {
|
||||||
|
public static demo = () => html`
|
||||||
|
<div style="padding: 24px; max-width: 1000px; background: #09090b;">
|
||||||
|
<sg-tokens-view
|
||||||
|
.tokens=${[
|
||||||
|
{ id: 't1', name: 'CI/CD Pipeline', tokenPrefix: 'sg_abc', protocols: ['npm', 'oci'], scopes: [{ protocol: '*', actions: ['read', 'write'] }], expiresAt: '2027-01-01', lastUsedAt: '2026-03-19', usageCount: 245, createdAt: '2026-01-15' },
|
||||||
|
{ id: 't2', name: 'Read-only Mirror', tokenPrefix: 'sg_def', protocols: ['npm'], scopes: [{ protocol: 'npm', actions: ['read'] }], lastUsedAt: '2026-03-18', usageCount: 1230, createdAt: '2025-11-01' },
|
||||||
|
{ id: 't3', name: 'Deploy Token', tokenPrefix: 'sg_ghi', protocols: ['oci'], scopes: [{ protocol: 'oci', actions: ['read', 'write'] }], organizationId: 'org1', expiresAt: '2026-06-01', usageCount: 56, createdAt: '2026-02-20' },
|
||||||
|
]}
|
||||||
|
.organizations=${[
|
||||||
|
{ id: 'org1', name: 'myorg', displayName: 'My Organization', isPublic: true, memberCount: 8, createdAt: '2025-06-01' },
|
||||||
|
]}
|
||||||
|
></sg-tokens-view>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
public static demoGroups = ['Auth'];
|
||||||
|
|
||||||
|
@property({ type: Array })
|
||||||
|
public accessor tokens: ISgToken[] = [];
|
||||||
|
|
||||||
|
@property({ type: Array })
|
||||||
|
public accessor organizations: ISgOrganization[] = [];
|
||||||
|
|
||||||
|
private showCreateForm = false;
|
||||||
|
private createName = '';
|
||||||
|
private createProtocols: TSgProtocol[] = [];
|
||||||
|
private createOrgId = '';
|
||||||
|
private createExpiryDays = 0;
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
border: none;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-btn:hover {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Create form */
|
||||||
|
.create-form {
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
padding: 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#ddd')};
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#0a0a0a')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#ddd', '#333')};
|
||||||
|
font-size: 14px;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
outline: none;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:focus {
|
||||||
|
border-color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-select {
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#0a0a0a')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#ddd', '#333')};
|
||||||
|
font-size: 14px;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
outline: none;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.protocol-selector {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.protocol-chip {
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 150ms ease;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.protocol-chip:hover {
|
||||||
|
border-color: ${cssManager.bdTheme('#999', '#666')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.protocol-chip.selected {
|
||||||
|
background: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
color: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
border-color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-submit {
|
||||||
|
padding: 8px 20px;
|
||||||
|
background: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
border: none;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-submit:hover {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-cancel {
|
||||||
|
padding: 8px 20px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#ddd', '#333')};
|
||||||
|
font-size: 13px;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-cancel:hover {
|
||||||
|
border-color: ${cssManager.bdTheme('#999', '#666')};
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Token list */
|
||||||
|
.token-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px;
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
border-bottom: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-name-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-prefix {
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
color: ${cssManager.bdTheme('#888', '#777')};
|
||||||
|
background: ${cssManager.bdTheme('#f5f5f5', '#1a1a1a')};
|
||||||
|
padding: 1px 6px;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-protocols {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-protocol {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 1px 6px;
|
||||||
|
background: ${cssManager.bdTheme('#f5f5f5', '#1a1a1a')};
|
||||||
|
color: ${cssManager.bdTheme('#666', '#aaa')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-meta {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: ${cssManager.bdTheme('#888', '#777')};
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-expired {
|
||||||
|
color: #ef4444;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.revoke-btn {
|
||||||
|
padding: 6px 14px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #ef4444;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.revoke-btn:hover {
|
||||||
|
background: rgba(239, 68, 68, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 48px 32px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: ${cssManager.bdTheme('#888', '#777')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#111')};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
public render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<div class="page-title">API Tokens</div>
|
||||||
|
<button class="create-btn" @click=${() => { this.showCreateForm = true; this.requestUpdate(); }}>
|
||||||
|
+ New Token
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${this.showCreateForm ? this.renderCreateForm() : ''}
|
||||||
|
|
||||||
|
${this.tokens.length > 0
|
||||||
|
? html`
|
||||||
|
<div class="token-list">
|
||||||
|
${this.tokens.map((token) => this.renderToken(token))}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: html`<div class="empty-state">No API tokens created yet. Create one to authenticate with the registry.</div>`}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderCreateForm(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<div class="create-form">
|
||||||
|
<div class="form-title">Create New Token</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Token Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-input"
|
||||||
|
placeholder="e.g., CI/CD Pipeline"
|
||||||
|
@input=${(e: InputEvent) => { this.createName = (e.target as HTMLInputElement).value; }}
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Protocols</label>
|
||||||
|
<div class="protocol-selector">
|
||||||
|
${ALL_PROTOCOLS.map(
|
||||||
|
(proto) => html`
|
||||||
|
<button
|
||||||
|
class="protocol-chip ${this.createProtocols.includes(proto) ? 'selected' : ''}"
|
||||||
|
@click=${() => this.toggleProtocol(proto)}
|
||||||
|
>${proto}</button>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${this.organizations.length > 0
|
||||||
|
? html`
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Organization (optional)</label>
|
||||||
|
<select
|
||||||
|
class="form-select"
|
||||||
|
@change=${(e: Event) => { this.createOrgId = (e.target as HTMLSelectElement).value; }}
|
||||||
|
>
|
||||||
|
<option value="">No organization (personal token)</option>
|
||||||
|
${this.organizations.map(
|
||||||
|
(org) => html`<option value=${org.id}>${org.displayName || org.name}</option>`
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: ''}
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Expires In (days, 0 = never)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
class="form-input"
|
||||||
|
placeholder="0"
|
||||||
|
min="0"
|
||||||
|
@input=${(e: InputEvent) => { this.createExpiryDays = parseInt((e.target as HTMLInputElement).value) || 0; }}
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="form-submit" @click=${this.handleCreate}>Create Token</button>
|
||||||
|
<button class="form-cancel" @click=${() => { this.showCreateForm = false; this.requestUpdate(); }}>Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderToken(token: ISgToken): TemplateResult {
|
||||||
|
const isExpired = token.expiresAt && new Date(token.expiresAt) < new Date();
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="token-row">
|
||||||
|
<div class="token-info">
|
||||||
|
<div class="token-name-row">
|
||||||
|
<span class="token-name">${token.name}</span>
|
||||||
|
<span class="token-prefix">${token.tokenPrefix}...</span>
|
||||||
|
</div>
|
||||||
|
<div class="token-protocols">
|
||||||
|
${token.protocols.map(
|
||||||
|
(p) => html`<span class="token-protocol">${p}</span>`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="token-meta">
|
||||||
|
<span>Created ${this.formatDate(token.createdAt)}</span>
|
||||||
|
${token.lastUsedAt ? html`<span>Last used ${this.formatDate(token.lastUsedAt)}</span>` : ''}
|
||||||
|
<span>${token.usageCount} uses</span>
|
||||||
|
${token.expiresAt
|
||||||
|
? isExpired
|
||||||
|
? html`<span class="token-expired">Expired</span>`
|
||||||
|
: html`<span>Expires ${this.formatDate(token.expiresAt)}</span>`
|
||||||
|
: html`<span>No expiry</span>`}
|
||||||
|
${token.organizationId ? html`<span>Org-scoped</span>` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="token-actions">
|
||||||
|
<button class="revoke-btn" @click=${() => this.handleRevoke(token.id)}>Revoke</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private toggleProtocol(proto: TSgProtocol) {
|
||||||
|
if (this.createProtocols.includes(proto)) {
|
||||||
|
this.createProtocols = this.createProtocols.filter((p) => p !== proto);
|
||||||
|
} else {
|
||||||
|
this.createProtocols = [...this.createProtocols, proto];
|
||||||
|
}
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleCreate() {
|
||||||
|
if (!this.createName.trim()) return;
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('create', {
|
||||||
|
detail: {
|
||||||
|
name: this.createName.trim(),
|
||||||
|
protocols: this.createProtocols,
|
||||||
|
scopes: [{ protocol: '*' as const, actions: ['read', 'write'] as const }],
|
||||||
|
organizationId: this.createOrgId || undefined,
|
||||||
|
expiresInDays: this.createExpiryDays || undefined,
|
||||||
|
},
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.showCreateForm = false;
|
||||||
|
this.createName = '';
|
||||||
|
this.createProtocols = [];
|
||||||
|
this.createOrgId = '';
|
||||||
|
this.createExpiryDays = 0;
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleRevoke(tokenId: string) {
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('revoke', {
|
||||||
|
detail: { tokenId },
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatDate(dateStr: string): string {
|
||||||
|
if (!dateStr) return '';
|
||||||
|
try {
|
||||||
|
return new Date(dateStr).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
|
||||||
|
} catch {
|
||||||
|
return dateStr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
ts_web/index.ts
Normal file
2
ts_web/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './elements/index.js';
|
||||||
|
export * from './pages/index.js';
|
||||||
160
ts_web/interfaces.ts
Normal file
160
ts_web/interfaces.ts
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
// ============================================================================
|
||||||
|
// Catalog Component Interfaces
|
||||||
|
// These are the prop shapes accepted by sg-* components.
|
||||||
|
// They are decoupled from backend models for reusability.
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export type TSgProtocol = 'oci' | 'npm' | 'maven' | 'cargo' | 'composer' | 'pypi' | 'rubygems';
|
||||||
|
|
||||||
|
export type TSgOrgRole = 'owner' | 'admin' | 'member';
|
||||||
|
|
||||||
|
export interface ISgStat {
|
||||||
|
title: string;
|
||||||
|
value: string | number;
|
||||||
|
icon?: string;
|
||||||
|
trend?: 'up' | 'down' | 'neutral';
|
||||||
|
trendValue?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISgOrganization {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
displayName: string;
|
||||||
|
description?: string;
|
||||||
|
avatarUrl?: string;
|
||||||
|
isPublic: boolean;
|
||||||
|
memberCount: number;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISgOrganizationDetail extends ISgOrganization {
|
||||||
|
website?: string;
|
||||||
|
usedStorageBytes: number;
|
||||||
|
storageQuotaBytes: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISgOrganizationMember {
|
||||||
|
userId: string;
|
||||||
|
role: TSgOrgRole;
|
||||||
|
addedAt: string;
|
||||||
|
user: {
|
||||||
|
username: string;
|
||||||
|
displayName: string;
|
||||||
|
avatarUrl?: string;
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISgOrgRedirect {
|
||||||
|
id: string;
|
||||||
|
oldName: string;
|
||||||
|
organizationId: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISgRepository {
|
||||||
|
id: string;
|
||||||
|
organizationId: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
protocol: TSgProtocol;
|
||||||
|
visibility: 'public' | 'private' | 'internal';
|
||||||
|
isPublic: boolean;
|
||||||
|
packageCount: number;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISgPackage {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
protocol: TSgProtocol;
|
||||||
|
organizationId: string;
|
||||||
|
repositoryId: string;
|
||||||
|
latestVersion?: string;
|
||||||
|
isPrivate: boolean;
|
||||||
|
downloadCount: number;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISgPackageDetail extends ISgPackage {
|
||||||
|
distTags: Record<string, string>;
|
||||||
|
versions: string[];
|
||||||
|
starCount: number;
|
||||||
|
storageBytes: number;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISgPackageVersion {
|
||||||
|
version: string;
|
||||||
|
publishedAt: string;
|
||||||
|
size: number;
|
||||||
|
downloads: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISgToken {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
tokenPrefix: string;
|
||||||
|
protocols: TSgProtocol[];
|
||||||
|
scopes: ISgTokenScope[];
|
||||||
|
organizationId?: string;
|
||||||
|
expiresAt?: string;
|
||||||
|
lastUsedAt?: string;
|
||||||
|
usageCount: number;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISgTokenScope {
|
||||||
|
protocol: TSgProtocol | '*';
|
||||||
|
organizationId?: string;
|
||||||
|
actions: ('read' | 'write' | 'delete' | '*')[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISgUser {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
username: string;
|
||||||
|
displayName: string;
|
||||||
|
avatarUrl?: string;
|
||||||
|
isSystemAdmin: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISgSession {
|
||||||
|
id: string;
|
||||||
|
userAgent: string;
|
||||||
|
ipAddress: string;
|
||||||
|
isValid: boolean;
|
||||||
|
lastActivityAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISgAuthProvider {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
displayName: string;
|
||||||
|
type: 'oidc' | 'ldap';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISgAuthProviderDetail extends ISgAuthProvider {
|
||||||
|
status: 'active' | 'disabled' | 'testing';
|
||||||
|
priority: number;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
lastTestedAt?: string;
|
||||||
|
lastTestResult?: 'success' | 'failure';
|
||||||
|
lastTestError?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISgPlatformSettings {
|
||||||
|
localAuthEnabled: boolean;
|
||||||
|
allowUserRegistration: boolean;
|
||||||
|
sessionDurationMinutes: number;
|
||||||
|
defaultProviderId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISgDashboardStats {
|
||||||
|
organizationCount: number;
|
||||||
|
packageCount: number;
|
||||||
|
totalDownloads: number;
|
||||||
|
tokenCount: number;
|
||||||
|
}
|
||||||
1
ts_web/pages/index.ts
Normal file
1
ts_web/pages/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './mainpage.js';
|
||||||
43
ts_web/pages/mainpage.ts
Normal file
43
ts_web/pages/mainpage.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { html } from '@design.estate/dees-element';
|
||||||
|
|
||||||
|
export const mainpage = () => html`
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: #111;
|
||||||
|
}
|
||||||
|
.demo-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
.demo-section {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 1px solid #333;
|
||||||
|
padding: 48px;
|
||||||
|
max-width: 600px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
font-size: 28px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="demo-container">
|
||||||
|
<div class="demo-section">
|
||||||
|
<h1>Stack.Gallery Catalog</h1>
|
||||||
|
<p>Component catalog for the Stack.Gallery Registry UI. Use <code>tswatch</code> to start the development server.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
13
tsconfig.json
Normal file
13
tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"dist_*/**/*.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user