diff --git a/index.html b/index.html
index a541d09afa5639db330494a398d7159eac29d7a8..b1c086f1f35686da4c8693c6b2525428caf81da5 100644
--- a/index.html
+++ b/index.html
@@ -17,7 +17,20 @@
- Button
+
+ Primary Button
+ Default Button
+ Dashed Button
+ Text Button
+ Link Button
+ Large Button
+ Small Button
+ Loading Button
+ Disabled Button
+ Danger Button
+
+
+
diff --git a/scripts/build.js b/scripts/build.js
index 961e252c4e6fb525076d090c6292a58c67caf744..61220ad95d56ced7674ee7685bebeb9eb090c79c 100644
--- a/scripts/build.js
+++ b/scripts/build.js
@@ -1,4 +1,5 @@
import webpack from "webpack";
+import path from "path";
import fs from "node:fs";
import atImport from "postcss-import";
@@ -14,7 +15,7 @@ async function css() {
// cssnano(),
]).process(content, { from: "src/main.css", to: "dist/main.css" });
- fs.writeFileSync("dist/all.css", output.css, "utf8");
+ fs.writeFileSync(path.join(__dirname, "..", "dist", "all.css"), output.css, "utf8");
}
async function js() {
@@ -25,7 +26,7 @@ async function js() {
// context: ".",
resolve: { extensions: [".ts", ".css"] },
output: {
- path: __dirname + "dist",
+ path: path.join(__dirname, "..", "dist"),
filename: "index.js",
library: "ui3",
},
@@ -46,7 +47,7 @@ async function js() {
}
try {
- fs.mkdirSync("dist");
+ fs.mkdirSync(path.join(__dirname, "..", "dist"));
} catch {}
js();
diff --git a/src/Button/index.css b/src/Button/index.css
deleted file mode 100644
index c7cf6a086f70987c3bdb6bfc1b23872c45c097a0..0000000000000000000000000000000000000000
--- a/src/Button/index.css
+++ /dev/null
@@ -1,51 +0,0 @@
-ct-button {
- font-family: var(--ct-font-family);
- color: var(--ct-color-success);
- font-size: var(--ct-font-size);
- border-radius: var(--ct-border-radius);
- cursor: pointer;
- scrollbar-color: unset;
- direction: initial;
- margin: 0;
- box-sizing: border-box;
- outline: none;
- position: relative;
- display: inline-flex;
- white-space: nowrap;
- text-align: center;
- user-select: none;
- touch-action: manipulation;
- padding: 8px;
- background: rgb(22, 119, 255);
- color: white;
- box-shadow: rgba(5, 145, 255, 0.1) 0px 2px 0px 0px;
-
- &:focus:not([disabled]),
- &:focus-visible {
- outline: 2px solid #bde4ff;
- }
-
- &:focus:not(:focus-visible) {
- outline: 0;
- }
-
- &:hover {
- /* if you extend, please overwrite */
- background: #f4f6f7;
- }
-
- &:active {
- background: gray;
- }
-
- &[hidden] {
- display: none;
- }
-
- &[disabled] {
- pointer-events: none;
- background: lightgray;
- color: #adadad;
- fill: #adadad;
- }
-}
diff --git a/src/Button/index.ts b/src/Button/index.ts
index c49e32fc2637e73a0bab56f148fb3491b4795cc6..4fcb36618368cee52b3f8607c4d6152a42d17cff 100644
--- a/src/Button/index.ts
+++ b/src/Button/index.ts
@@ -1,12 +1,318 @@
-import { html, LitElement } from "lit";
+import { html, LitElement, css } from "lit";
import { property } from "lit/decorators.js";
+import { classMap } from "lit/directives/class-map.js";
+import "../Icon/Loading";
+
+export type ButtonType = 'primary' | 'default' | 'dashed' | 'text' | 'link';
+export type ButtonSize = 'large' | 'middle' | 'small';
+
+// 定义组件自身的属性
+const BUTTON_PROPERTIES = new Set([
+ 'type',
+ 'size',
+ 'danger',
+ 'disabled',
+ 'loading',
+ 'icon',
+ 'href'
+]);
+
+// 定义原生button允许的属性
+const NATIVE_PROPERTIES = new Set([
+ 'type',
+ 'name',
+ 'form',
+ 'formAction',
+ 'formEnctype',
+ 'formMethod',
+ 'formNoValidate',
+ 'formTarget',
+ 'autofocus',
+ 'value',
+ 'tabindex',
+ 'title',
+ 'ariaLabel',
+ 'ariaDescribedby'
+]);
+
+export class Button extends LitElement {
+ static styles = css`
+ :host {
+ display: inline-block;
+ }
+
+ .ct-btn {
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ box-sizing: border-box;
+ padding: 4px 15px;
+ font-size: 13px;
+ border-radius: var(--ct-border-radius-small);
+ border: 1px solid var(--ct-color-border);
+ cursor: pointer;
+ transition: all 0.2s;
+ user-select: none;
+ text-decoration: none;
+ }
+
+ /* Types */
+ .ct-btn--primary {
+ color: white;
+ background: var(--ct-color-primary);
+ border-color: var(--ct-color-primary);
+ }
+
+ .ct-btn--primary:hover {
+ background: var(--ct-color-primary-hover);
+ border-color: var(--ct-color-primary-hover);
+ }
+
+ .ct-btn--primary:active {
+ background: var(--ct-color-primary-active);
+ border-color: var(--ct-color-primary-active);
+ }
+
+ .ct-btn--default {
+ background: var(--ct-color-fill-0);
+ border-color: var(--ct-color-border);
+ color: var(--ct-color-text-0);
+ }
+
+ .ct-btn--default:hover {
+ color: var(--ct-color-primary);
+ border-color: var(--ct-color-primary);
+ }
+
+ .ct-btn--dashed {
+ background: transparent;
+ border: 1px dashed var(--ct-color-border);
+ color: var(--ct-color-text-0);
+ }
+
+ .ct-btn--dashed:hover {
+ color: var(--ct-color-primary);
+ border-color: var(--ct-color-primary);
+ }
+
+ .ct-btn--text {
+ background: transparent;
+ border-color: transparent;
+ color: var(--ct-color-primary);
+ }
+
+ .ct-btn--text:hover {
+ background: var(--ct-color-light-hover);
+ }
+
+ .ct-btn--text:active {
+ background: var(--ct-color-light-active);
+ }
+
+ .ct-btn--link {
+ background: transparent;
+ border-color: transparent;
+ color: var(--ct-color-primary);
+ padding: 0;
+ }
+
+ .ct-btn--link:hover {
+ color: var(--ct-color-primary-hover);
+ }
+
+ /* Sizes */
+ .ct-btn--large {
+ padding: 6px 20px;
+ font-size: 16px;
+ }
+
+ .ct-btn--small {
+ padding: 2px 12px;
+ font-size: 12px;
+ }
+
+ /* States */
+ .ct-btn--danger {
+ color: var(--ct-color-danger);
+ border-color: var(--ct-color-danger);
+ }
+
+ .ct-btn--danger:hover {
+ color: var(--ct-color-danger-hover);
+ border-color: var(--ct-color-danger-hover);
+ }
+
+ .ct-btn--danger.ct-btn--primary {
+ background: var(--ct-color-danger);
+ border-color: var(--ct-color-danger);
+ color: white;
+ }
+
+ .ct-btn--danger.ct-btn--primary:hover {
+ background: var(--ct-color-danger-hover);
+ border-color: var(--ct-color-danger-hover);
+ }
+
+ .ct-btn:disabled {
+ cursor: not-allowed;
+ opacity: 0.65;
+ }
+
+ /* Loading */
+ .ct-btn--loading {
+ position: relative;
+ cursor: not-allowed;
+ }
+
+ .ct-btn__icon {
+ display: inline-flex;
+ align-items: center;
+ margin-right: 8px;
+ }
+
+ .ct-btn__loading-icon {
+ display: inline-flex;
+ margin-right: 8px;
+ }
+
+ .ct-btn--small .ct-btn__loading-icon {
+ width: 12px;
+ height: 12px;
+ }
+
+ .ct-btn--large .ct-btn__loading-icon {
+ width: 16px;
+ height: 16px;
+ }
+
+ /* Active state */
+ :host(:active),
+ :host([active]) {
+ transform: scale(0.98);
+ }
+
+ .ct-btn:active,
+ .ct-btn[active] {
+ transform: scale(0.98);
+ }
+ `;
+
+ // 组件自身属性
+ @property({ type: String, reflect: true })
+ type: ButtonType = 'default';
+
+ @property({ type: String, reflect: true })
+ size: ButtonSize = 'middle';
+
+ @property({ type: Boolean, reflect: true })
+ danger = false;
+
+ @property({ type: Boolean, reflect: true })
+ disabled = false;
+
+ @property({ type: Boolean })
+ loading = false;
+
+ @property()
+ icon?: string;
-class Button extends LitElement {
@property()
- type?: "link";
+ href?: string;
+
+ @property({ type: Boolean, reflect: true })
+ active = false;
+
+ // 收集所有原生属性
+ private get buttonProps() {
+ const props: Record = {};
+
+ // 获取所有属性
+ const attrs = this.attributes;
+ for (let i = 0; i < attrs.length; i++) {
+ const attr = attrs[i];
+ const propName = attr.name;
+
+ // 跳过组件自身的属性
+ if (BUTTON_PROPERTIES.has(propName)) {
+ continue;
+ }
+
+ // 只收集原生button允许的属性
+ if (NATIVE_PROPERTIES.has(propName)) {
+ props[propName] = attr.value;
+ }
+ }
+
+ return props;
+ }
+
+ private renderLoadingIcon() {
+ return html`
+
+
+
+ `;
+ }
+
+ // 更新 handleClick 方法
+ private handleClick(e: MouseEvent) {
+ // 阻止事件冒泡到 ct-button 元素
+ e.stopPropagation();
+
+ if (this.disabled || this.loading) {
+ e.preventDefault();
+ return;
+ }
+
+ // 分发点击事件
+ this.dispatchEvent(new CustomEvent('click', {
+ bubbles: true,
+ composed: true,
+ cancelable: true,
+ detail: { originalEvent: e }
+ }));
+ }
render() {
- return html``;
+ const classes = {
+ 'ct-btn': true,
+ [`ct-btn--${this.type}`]: true,
+ [`ct-btn--${this.size}`]: this.size !== 'middle',
+ 'ct-btn--danger': this.danger,
+ };
+
+ const content = html`
+ ${this.loading ? this.renderLoadingIcon() : null}
+ ${!this.loading && this.icon ? html`${this.icon}` : null}
+
+ `;
+
+ if (this.href && this.type === 'link') {
+ return html`
+
+ ${content}
+
+ `;
+ }
+
+ return html`
+
+ `;
}
connectedCallback() {
@@ -15,4 +321,4 @@ class Button extends LitElement {
}
}
-export default Button;
+export default Button;
\ No newline at end of file
diff --git a/src/Icon/Loading/index.ts b/src/Icon/Loading/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b0f18f4ba411efe6609122072513e78b891d33ac
--- /dev/null
+++ b/src/Icon/Loading/index.ts
@@ -0,0 +1,42 @@
+import { html, css, unsafeCSS } from "lit";
+import { Icon } from "../index";
+
+export class IconLoading extends Icon {
+ static styles = css`
+ ${unsafeCSS(Icon.styles)}
+ @keyframes rotate {
+ 100% {
+ transform: rotate(360deg);
+ }
+ }
+
+ :host .ct-icon {
+ animation: rotate 1s linear infinite;
+ width: inherit;
+ height: inherit;
+ }
+ `;
+
+ constructor() {
+ super();
+ this.type = 'loading';
+ }
+
+ render() {
+ return html`
+
+ `;
+ }
+}
+
+customElements.define('ct-icon-loading', IconLoading);
+
+export default IconLoading;
\ No newline at end of file
diff --git a/src/Icon/index.ts b/src/Icon/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8479971f72e7d41ad7bfe85af6cfa53df9a2e455
--- /dev/null
+++ b/src/Icon/index.ts
@@ -0,0 +1,38 @@
+import { html, LitElement, css } from "lit";
+import { property } from "lit/decorators.js";
+
+export class Icon extends LitElement {
+ static styles = css`
+ :host {
+ display: inline-flex;
+ }
+
+ /* 默认尺寸 14px */
+ :host([size="middle"]) .ct-icon {
+ width: 14px;
+ height: 14px;
+ }
+
+ /* 小尺寸 12px */
+ :host([size="small"]) .ct-icon {
+ width: 12px;
+ height: 12px;
+ }
+
+ /* 大尺寸 16px */
+ :host([size="large"]) .ct-icon {
+ width: 16px;
+ height: 16px;
+ }
+ `;
+
+ @property({ type: String, reflect: true })
+ size: 'small' | 'middle' | 'large' = 'middle';
+
+ @property({ type: String })
+ type: string = '';
+}
+
+customElements.define('ct-icon', Icon);
+
+export default Icon;
\ No newline at end of file