mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
feat(elements): add components
This commit is contained in:
parent
bf139e5a8a
commit
0fec4556f9
15 changed files with 1024 additions and 12 deletions
14
packages/elements/index.html
Normal file
14
packages/elements/index.html
Normal file
|
@ -0,0 +1,14 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Logto elements dev page</title>
|
||||
<script type="module" src="./src/index.ts"></script>
|
||||
</head>
|
||||
<body style="background: #111;">
|
||||
<logto-theme-provider theme="dark">
|
||||
<logto-profile-card></logto-profile-card>
|
||||
</logto-theme-provider>
|
||||
</body>
|
||||
</html>
|
|
@ -8,6 +8,7 @@
|
|||
"type": "module",
|
||||
"private": true,
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
|
@ -28,12 +29,14 @@
|
|||
"scripts": {
|
||||
"precommit": "lint-staged",
|
||||
"build": "lit-localize build && tsup",
|
||||
"start": "web-dev-server",
|
||||
"dev": "lit-localize build && tsup --watch --no-splitting",
|
||||
"lint": "eslint --ext .ts src",
|
||||
"lint:report": "pnpm lint --format json --output-file report.json",
|
||||
"test": "echo \"No tests yet.\"",
|
||||
"test:ci": "pnpm run test --silent --coverage",
|
||||
"prepack": "pnpm check && pnpm build",
|
||||
"prepublishOnly": "pnpm check",
|
||||
"prepack": "pnpm build",
|
||||
"localize": "lit-localize",
|
||||
"check": "lit-localize extract && git add . -N && git diff --exit-code"
|
||||
},
|
||||
|
@ -53,6 +56,8 @@
|
|||
"@lit/localize-tools": "^0.7.2",
|
||||
"@silverhand/eslint-config": "6.0.1",
|
||||
"@silverhand/ts-config": "6.0.0",
|
||||
"@web/dev-server": "^0.4.6",
|
||||
"@web/dev-server-esbuild": "^1.0.2",
|
||||
"eslint": "^8.56.0",
|
||||
"lint-staged": "^15.0.0",
|
||||
"prettier": "^3.0.0",
|
||||
|
|
53
packages/elements/src/components/logto-avatar.ts
Normal file
53
packages/elements/src/components/logto-avatar.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
import { msg } from '@lit/localize';
|
||||
import { LitElement, css } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
|
||||
import { unit } from '../utils/css.js';
|
||||
|
||||
const tagName = 'logto-avatar';
|
||||
|
||||
const sizes = Object.freeze({
|
||||
medium: unit(8),
|
||||
large: unit(10),
|
||||
});
|
||||
|
||||
@customElement(tagName)
|
||||
export class LogtoAvatar extends LitElement {
|
||||
static tagName = tagName;
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
border-radius: ${unit(2)};
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
`;
|
||||
|
||||
@property({ reflect: true })
|
||||
size: 'medium' | 'large' = 'medium';
|
||||
|
||||
@property({ reflect: true })
|
||||
src = '';
|
||||
|
||||
@property({ reflect: true })
|
||||
alt = msg('Avatar', {
|
||||
id: 'account.profile.personal-info.avatar',
|
||||
desc: 'The avatar of the user.',
|
||||
});
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.style.setProperty('width', sizes[this.size].cssText);
|
||||
this.style.setProperty('height', sizes[this.size].cssText);
|
||||
|
||||
if (this.src) {
|
||||
// Show the image holder with the provided image.
|
||||
this.style.setProperty('background-color', '#adaab422');
|
||||
this.style.setProperty('background-image', `url(${this.src})`);
|
||||
} else {
|
||||
// A temporary default fallback color. Need to implement the relevant logic in `<UserAvatar />` later.
|
||||
this.style.setProperty('background-color', '#e74c3c');
|
||||
}
|
||||
}
|
||||
}
|
67
packages/elements/src/components/logto-button.ts
Normal file
67
packages/elements/src/components/logto-button.ts
Normal file
|
@ -0,0 +1,67 @@
|
|||
import { LitElement, css, html } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
|
||||
import { unit } from '../utils/css.js';
|
||||
import { vars } from '../utils/theme.js';
|
||||
|
||||
const tagName = 'logto-button';
|
||||
|
||||
@customElement(tagName)
|
||||
export class LogtoButton extends LitElement {
|
||||
static tagName = tagName;
|
||||
static styles = css`
|
||||
:host {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
outline: none;
|
||||
font: ${vars.fontLabel2};
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
gap: ${unit(2)};
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:host(:disabled) {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
:host([type='text']) {
|
||||
background: none;
|
||||
border-color: none;
|
||||
font: ${vars.fontLabel2};
|
||||
color: ${vars.colorTextLink};
|
||||
padding: ${unit(0.5, 1)};
|
||||
border-radius: ${unit(1)};
|
||||
}
|
||||
|
||||
:host([type='text']:disabled) {
|
||||
color: ${vars.colorDisabled};
|
||||
}
|
||||
|
||||
:host([type='text']:focus-visible) {
|
||||
outline: 2px solid ${vars.colorFocusedVariant};
|
||||
}
|
||||
|
||||
:host([type='text']:not(:disabled):hover) {
|
||||
background: ${vars.colorHoverVariant};
|
||||
}
|
||||
`;
|
||||
|
||||
@property()
|
||||
type: 'default' | 'text' = 'default';
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.role = 'button';
|
||||
this.tabIndex = 0;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<slot></slot>`;
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ import { vars } from '../utils/theme.js';
|
|||
|
||||
const tagName = 'logto-card-section';
|
||||
|
||||
/** A section in a form card with a heading. It is used to group related content. */
|
||||
@customElement(tagName)
|
||||
@localized()
|
||||
export class LogtoCardSection extends LitElement {
|
||||
|
@ -14,7 +15,7 @@ export class LogtoCardSection extends LitElement {
|
|||
static styles = css`
|
||||
header {
|
||||
font: ${vars.fontLabel2};
|
||||
color: ${vars.colorText};
|
||||
color: ${vars.colorTextPrimary};
|
||||
margin-bottom: ${unit(1)};
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -6,6 +6,16 @@ import { vars } from '../utils/theme.js';
|
|||
|
||||
const tagName = 'logto-card';
|
||||
|
||||
/**
|
||||
* A card with background, padding, and border radius.
|
||||
*
|
||||
* @example
|
||||
* ```html
|
||||
* <logto-card>
|
||||
* <!-- Content goes here -->
|
||||
* </logto-card>
|
||||
* ```
|
||||
*/
|
||||
@customElement(tagName)
|
||||
export class LogtoCard extends LitElement {
|
||||
static tagName = tagName;
|
||||
|
|
|
@ -8,6 +8,26 @@ import { vars } from '../utils/theme.js';
|
|||
|
||||
const tagName = 'logto-form-card';
|
||||
|
||||
/**
|
||||
* A card that contains a form or form-like content. A heading and an optional description can be
|
||||
* provided to describe the purpose of the content.
|
||||
*
|
||||
* To group related content in a form card, use the `logto-card-section` element.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* html`
|
||||
* <logto-form-card heading=${msg('Account', ...)}>
|
||||
* <logto-card-section heading=${msg('Personal information', ...)}>
|
||||
* <!-- Content goes here -->
|
||||
* </logto-card-section>
|
||||
* <logto-card-section heading=${msg('Account settings', ...)}>
|
||||
* <!-- Content goes here -->
|
||||
* </logto-card-section>
|
||||
* </logto-form-card>
|
||||
* `
|
||||
* ```
|
||||
*/
|
||||
@customElement(tagName)
|
||||
@localized()
|
||||
export class LogtoFormCard extends LitElement {
|
||||
|
@ -30,7 +50,8 @@ export class LogtoFormCard extends LitElement {
|
|||
flex: 1;
|
||||
}
|
||||
|
||||
::slotted(*) {
|
||||
slot {
|
||||
display: block;
|
||||
flex: 16;
|
||||
}
|
||||
`;
|
||||
|
|
50
packages/elements/src/components/logto-list-row.ts
Normal file
50
packages/elements/src/components/logto-list-row.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
import { localized, msg } from '@lit/localize';
|
||||
import { LitElement, css, html } from 'lit';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
|
||||
import { unit } from '../utils/css.js';
|
||||
import { vars } from '../utils/theme.js';
|
||||
|
||||
const tagName = 'logto-list-row';
|
||||
|
||||
@customElement(tagName)
|
||||
@localized()
|
||||
export class LogtoListRow extends LitElement {
|
||||
static tagName = tagName;
|
||||
static styles = css`
|
||||
:host {
|
||||
box-sizing: border-box;
|
||||
display: grid;
|
||||
height: ${unit(16)};
|
||||
padding: ${unit(2, 6)};
|
||||
grid-template-columns: 1fr 2fr 1fr;
|
||||
align-items: center;
|
||||
color: ${vars.colorTextPrimary};
|
||||
font: ${vars.fontBody2};
|
||||
}
|
||||
|
||||
:host(:not(:last-child)) {
|
||||
border-bottom: 1px solid ${vars.colorLineDivider};
|
||||
}
|
||||
|
||||
slot {
|
||||
display: block;
|
||||
}
|
||||
|
||||
slot[name='title'] {
|
||||
font: ${vars.fontLabel2};
|
||||
}
|
||||
|
||||
slot[name='actions'] {
|
||||
text-align: right;
|
||||
}
|
||||
`;
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<slot name="title">{${msg('Title', { id: 'general.title' })}}</slot>
|
||||
<slot name="content">{${msg('Content', { id: 'general.content' })}}</slot>
|
||||
<slot name="actions">{${msg('Actions', { id: 'general.actions' })}}</slot>
|
||||
`;
|
||||
}
|
||||
}
|
23
packages/elements/src/components/logto-list.ts
Normal file
23
packages/elements/src/components/logto-list.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { LitElement, css, html } from 'lit';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
|
||||
import { unit } from '../utils/css.js';
|
||||
import { vars } from '../utils/theme.js';
|
||||
|
||||
const tagName = 'logto-list';
|
||||
|
||||
@customElement(tagName)
|
||||
export class LogtoList extends LitElement {
|
||||
static tagName = tagName;
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
border-radius: ${unit(2)};
|
||||
border: 1px solid ${vars.colorLineDivider};
|
||||
}
|
||||
`;
|
||||
|
||||
render() {
|
||||
return html`<slot></slot>`;
|
||||
}
|
||||
}
|
|
@ -11,7 +11,7 @@ const tagName = 'logto-profile-card';
|
|||
export class LogtoProfileCard extends LitElement {
|
||||
static tagName = tagName;
|
||||
static styles = css`
|
||||
p {
|
||||
p.dev {
|
||||
color: ${vars.colorTextSecondary};
|
||||
}
|
||||
`;
|
||||
|
@ -19,10 +19,42 @@ export class LogtoProfileCard extends LitElement {
|
|||
render() {
|
||||
return html`
|
||||
<logto-form-card heading=${msg('Profile', { id: 'account.profile.title' })}>
|
||||
<p class="dev">🚧 This section is a dev feature that is still working in progress.</p>
|
||||
<logto-card-section
|
||||
heading=${msg('Personal information', { id: 'account.profile.personal-info.title' })}
|
||||
>
|
||||
<p>🚧 This section is a dev feature that is still working in progress.</p>
|
||||
<logto-list>
|
||||
<logto-list-row>
|
||||
<div slot="title">
|
||||
${msg('Avatar', {
|
||||
id: 'account.profile.personal-info.avatar',
|
||||
desc: 'The avatar of the user.',
|
||||
})}
|
||||
</div>
|
||||
<div slot="content">
|
||||
<logto-avatar size="large" src="https://github.com/logto-io.png"></logto-avatar>
|
||||
</div>
|
||||
<div slot="actions">
|
||||
<logto-button type="text">
|
||||
${msg('Change', { id: 'general.change' })}
|
||||
</logto-button>
|
||||
</div>
|
||||
</logto-list-row>
|
||||
<logto-list-row>
|
||||
<div slot="title">
|
||||
${msg('Name', {
|
||||
id: 'account.profile.personal-info.name',
|
||||
desc: 'The name of the user.',
|
||||
})}
|
||||
</div>
|
||||
<div slot="content">John Doe</div>
|
||||
<div slot="actions">
|
||||
<logto-button type="text">
|
||||
${msg('Change', { id: 'general.change' })}
|
||||
</logto-button>
|
||||
</div>
|
||||
</logto-list-row>
|
||||
</logto-list>
|
||||
</logto-card-section>
|
||||
</logto-form-card>
|
||||
`;
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
export * from './components/logto-avatar.js';
|
||||
export * from './components/logto-button.js';
|
||||
export * from './components/logto-card-section.js';
|
||||
export * from './components/logto-card.js';
|
||||
export * from './components/logto-form-card.js';
|
||||
export * from './components/logto-list-row.js';
|
||||
export * from './components/logto-list.js';
|
||||
export * from './components/logto-theme-provider.js';
|
||||
export * from './elements/logto-profile-card.js';
|
||||
export * from './utils/locale.js';
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
import { createComponent } from '@lit/react';
|
||||
|
||||
import { LogtoThemeProvider, LogtoCard, LogtoFormCard, LogtoProfileCard } from './index.js';
|
||||
import {
|
||||
LogtoThemeProvider,
|
||||
LogtoCard,
|
||||
LogtoFormCard,
|
||||
LogtoProfileCard,
|
||||
LogtoList,
|
||||
} from './index.js';
|
||||
|
||||
export * from './utils/locale.js';
|
||||
|
||||
|
@ -11,6 +17,11 @@ export const createReactComponents = (react: Parameters<typeof createComponent>[
|
|||
elementClass: LogtoFormCard,
|
||||
react,
|
||||
}),
|
||||
LogtoList: createComponent({
|
||||
tagName: LogtoList.tagName,
|
||||
elementClass: LogtoList,
|
||||
react,
|
||||
}),
|
||||
LogtoProfileCard: createComponent({
|
||||
tagName: LogtoProfileCard.tagName,
|
||||
elementClass: LogtoProfileCard,
|
||||
|
|
|
@ -5,13 +5,18 @@ import { type KebabCase, kebabCase } from './string.js';
|
|||
/** All the colors to be used in the Logto components and elements. */
|
||||
export type Color = {
|
||||
colorPrimary: string;
|
||||
colorText: string;
|
||||
colorTextPrimary: string;
|
||||
colorTextLink: string;
|
||||
colorTextSecondary: string;
|
||||
colorBorder: string;
|
||||
colorCardTitle: string;
|
||||
colorLayer1: string;
|
||||
colorLayer2: string;
|
||||
colorLineDivider: string;
|
||||
colorDisabled: string;
|
||||
colorHover: string;
|
||||
colorHoverVariant: string;
|
||||
colorFocusedVariant: string;
|
||||
};
|
||||
|
||||
/** All the fonts to be used in the Logto components and elements. */
|
||||
|
@ -19,6 +24,9 @@ export type Font = {
|
|||
fontLabel1: string;
|
||||
fontLabel2: string;
|
||||
fontLabel3: string;
|
||||
fontBody1: string;
|
||||
fontBody2: string;
|
||||
fontBody3: string;
|
||||
fontSectionHeading1: string;
|
||||
fontSectionHeading2: string;
|
||||
};
|
||||
|
@ -33,6 +41,9 @@ export const defaultFont: Readonly<Font> = Object.freeze({
|
|||
fontLabel1: `500 16px / 24px ${defaultFontFamily}`,
|
||||
fontLabel2: `500 14px / 20px ${defaultFontFamily}`,
|
||||
fontLabel3: `500 12px / 16px ${defaultFontFamily}`,
|
||||
fontBody1: `400 16px / 24px ${defaultFontFamily}`,
|
||||
fontBody2: `400 14px / 20px ${defaultFontFamily}`,
|
||||
fontBody3: `400 12px / 16px ${defaultFontFamily}`,
|
||||
fontSectionHeading1: `700 12px / 16px ${defaultFontFamily}`,
|
||||
fontSectionHeading2: `700 10px / 16px ${defaultFontFamily}`,
|
||||
});
|
||||
|
@ -40,25 +51,35 @@ export const defaultFont: Readonly<Font> = Object.freeze({
|
|||
export const defaultTheme: Readonly<Theme> = Object.freeze({
|
||||
...defaultFont,
|
||||
colorPrimary: '#5d34f2',
|
||||
colorText: '#191c1d',
|
||||
colorTextPrimary: '#191c1d',
|
||||
colorTextLink: '#5d34f2',
|
||||
colorTextSecondary: '#747778',
|
||||
colorBorder: '#c4c7c7',
|
||||
colorCardTitle: '#928f9a',
|
||||
colorLayer1: '#000',
|
||||
colorLayer2: '#2d3132',
|
||||
colorLineDivider: '#191c1d1f',
|
||||
colorDisabled: '#5c5f60',
|
||||
colorHover: '#191c1d14',
|
||||
colorHoverVariant: '#5d34f214',
|
||||
colorFocusedVariant: '#5d34f229',
|
||||
});
|
||||
|
||||
export const darkTheme: Readonly<Theme> = Object.freeze({
|
||||
...defaultFont,
|
||||
colorPrimary: '#7958ff',
|
||||
colorText: '#f7f8f8',
|
||||
colorTextPrimary: '#f7f8f8',
|
||||
colorTextLink: '#cabeff',
|
||||
colorTextSecondary: '#a9acac',
|
||||
colorBorder: '#5c5f60',
|
||||
colorCardTitle: '#928f9a',
|
||||
colorLayer1: '#2a2c32',
|
||||
colorLayer2: '#34353f',
|
||||
colorLineDivider: '#f7f8f824',
|
||||
colorDisabled: '#5c5f60',
|
||||
colorHover: '#f7f8f814',
|
||||
colorHoverVariant: '#cabeff14',
|
||||
colorFocusedVariant: '#cabeff29',
|
||||
});
|
||||
|
||||
/**
|
||||
|
@ -109,7 +130,7 @@ export const toVar = (value: string) => unsafeCSS(`var(--logto-${kebabCase(value
|
|||
* `
|
||||
*/
|
||||
// eslint-disable-next-line no-restricted-syntax -- `Object.fromEntries` will lose the type
|
||||
export const vars = Object.freeze(
|
||||
export const vars: Record<keyof Theme, CSSResult> = Object.freeze(
|
||||
Object.fromEntries(Object.keys(defaultTheme).map((key) => [key, toVar(key)]))
|
||||
) as Record<keyof Theme, CSSResult>;
|
||||
|
||||
|
|
21
packages/elements/web-dev-server.config.js
Normal file
21
packages/elements/web-dev-server.config.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
// eslint-disable-next-line unicorn/prevent-abbreviations
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { esbuildPlugin } from '@web/dev-server-esbuild';
|
||||
|
||||
const config = {
|
||||
open: true,
|
||||
watch: true,
|
||||
appIndex: 'index.html',
|
||||
nodeResolve: {
|
||||
exportConditions: ['development'],
|
||||
},
|
||||
plugins: [
|
||||
esbuildPlugin({
|
||||
ts: true,
|
||||
tsconfig: fileURLToPath(new URL('tsconfig.json', import.meta.url)),
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
683
pnpm-lock.yaml
683
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue