0
Fork 0
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:
Gao Sun 2024-07-16 14:55:47 +08:00
parent bf139e5a8a
commit 0fec4556f9
No known key found for this signature in database
GPG key ID: 13EBE123E4773688
15 changed files with 1024 additions and 12 deletions

View 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>

View file

@ -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",

View 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');
}
}
}

View 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>`;
}
}

View file

@ -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)};
}
`;

View file

@ -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;

View file

@ -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;
}
`;

View 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>
`;
}
}

View 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>`;
}
}

View file

@ -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>
`;

View file

@ -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';

View file

@ -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,

View file

@ -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>;

View 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;

File diff suppressed because it is too large Load diff