Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 182 additions & 0 deletions WEB_COMPONENTS_PLAN.md

Large diffs are not rendered by default.

37 changes: 36 additions & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions packages/elements/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# AGENTS.md — packages/elements

## 디렉토리 개요

SEED Design 컴포넌트의 **Lit 기반 Web Components** 구현체(`@seed-design/elements`). `@seed-design/css`의 recipe/토큰을 스타일로 재사용하고, headless 로직은 Lit으로 재구현한다. 클래스 기반 글로벌 CSS 모델에 맞춰 **Light DOM**으로 렌더한다. 상위 파이프라인은 루트 `TECH.md`, 설계 배경은 저장소 루트의 계획 문서를 참고.

## 파일 작성 컨벤션

- 컴포넌트 클래스(`Seed<Name>`)는 `src/components/<Name>/`에 정의하고, `customElements.define`은 `src/define/<name>.ts`로 분리한다(pure class export + 등록 분리).
- `package.json` `sideEffects`에 `**/*.css`와 `define/**`만 등록 → 클래스만 import하면 tree-shake되고, `@seed-design/elements/define/<name>`을 import해야 태그가 등록된다.
- 태그명은 `seed-` prefix(`seed-action-button`). barrel(`index.ts`) import는 전체 CSS를 끌어오므로 소비자에게 권하지 않는다.

## 코드 작성 컨벤션

- **Light DOM**: `createRenderRoot() { return this; }`. shadow DOM·`<slot>`·`::part`·`static styles` 미사용.
- **스타일**: `@seed-design/css`의 recipe 함수(`(props) => className`)를 호출해 클래스를 부여한다. 토큰은 `var(--seed-*)`. 컴포넌트별 CSS는 recipe의 self-import로 따라오며 `all.css`는 쓰지 않는다.
- **Non-interactive(presentational)**: host에 recipe class만 부여하고 `render()`를 정의하지 않는다(author children 보존).
- **Interactive**: author children을 inner native 요소(`<button>`/`<a>`)로 wrap해 native focus·키보드·`:disabled`를 활용한다. host-as-button(수동 role/tabindex) 금지.
- **Form control**: `ElementInternals`(`static formAssociated = true`)로 폼에 참여한다. lion식 slotted-input 방식은 쓰지 않는다.
- **decorators 미사용**: reactive property는 `static properties = { ... }` + 생성자 초기화로 선언한다(Vite 트랜스파일 호환). variant 타입은 `@seed-design/css`의 recipe 타입에서 가져온다.
68 changes: 68 additions & 0 deletions packages/elements/demo/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>SEED elements — demo</title>
<style>
body { font-family: system-ui, sans-serif; padding: 24px; max-width: 480px; }
section { margin: 20px 0; }
h2 { font-size: 13px; color: #888; margin: 0 0 8px; }
</style>
</head>
<body>
<section>
<h2>ActionButton</h2>
<seed-action-button id="ab" variant="brandSolid" size="medium">버튼 라벨</seed-action-button>
<seed-action-button variant="neutralWeak" size="small" disabled>비활성</seed-action-button>
</section>

<section>
<h2>Checkbox (in a form)</h2>
<form id="form">
<seed-checkbox id="cb" name="agree" value="yes">약관에 동의합니다</seed-checkbox>
</form>
</section>

<section>
<h2>Text / Skeleton / Badge</h2>
<p><seed-text text-style="t5Bold">제목 텍스트</seed-text></p>
<seed-skeleton radius="8" style="display:block;width:160px;height:16px;margin:8px 0"></seed-skeleton>
<div style="display:flex;gap:8px;margin-top:8px">
<seed-badge variant="solid" tone="positive">완료</seed-badge>
<seed-badge variant="weak" tone="warning">주의</seed-badge>
<seed-badge variant="outline" tone="critical">거절</seed-badge>
</div>
</section>

<section>
<h2>Switch</h2>
<seed-switch id="sw">알림 받기</seed-switch>
</section>

<section>
<h2>SegmentedControl</h2>
<seed-segmented-control id="seg" value="all">
<seed-segmented-control-item value="all">전체</seed-segmented-control-item>
<seed-segmented-control-item value="sell">판매중</seed-segmented-control-item>
<seed-segmented-control-item value="done">거래완료</seed-segmented-control-item>
</seed-segmented-control>
</section>

<section>
<h2>RadioGroup</h2>
<seed-radio-group id="rg" value="b" style="display:flex;flex-direction:column;gap:8px">
<seed-radio value="a">옵션 A</seed-radio>
<seed-radio value="b">옵션 B</seed-radio>
<seed-radio value="c">옵션 C</seed-radio>
</seed-radio-group>
</section>

<section>
<h2>TextField</h2>
<seed-text-field id="tf" placeholder="입력하세요"></seed-text-field>
</section>

<script type="module" src="/demo/main.ts"></script>
</body>
</html>
47 changes: 47 additions & 0 deletions packages/elements/demo/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import "@seed-design/css/base.css";

// All from consumer-owned snippets (layer 2).
import "./seed-design/ui/action-button";
import "./seed-design/ui/checkbox";
import "./seed-design/ui/text";
import "./seed-design/ui/skeleton";
import "./seed-design/ui/badge";
import "./seed-design/ui/switch";
import "./seed-design/ui/segmented-control";
import "./seed-design/ui/radio-group";
import "./seed-design/ui/text-field";

const w = window as unknown as { __results?: unknown };

customElements.whenDefined("seed-text-field").then(() => {
requestAnimationFrame(() => {
const sw = document.getElementById("sw") as (HTMLElement & { checked: boolean }) | null;
const seg = document.getElementById("seg") as (HTMLElement & { value: string }) | null;
const rg = document.getElementById("rg") as (HTMLElement & { value: string }) | null;
const tf = document.getElementById("tf") as (HTMLElement & { value: string }) | null;
if (!sw || !seg || !rg || !tf) return;

// drive each component programmatically
sw.checked = true;
seg.value = "sell";
rg.value = "c";
tf.value = "안녕";

requestAnimationFrame(() => {
const indicator = seg.querySelector<HTMLElement>(
'[class*="seed-segmented-control__indicator"]',
);
const selectedRadio = [...rg.querySelectorAll("seed-radio")].find((r) =>
r.hasAttribute("data-checked"),
);
w.__results = {
switch_checked: sw.checked,
segmented_value: seg.value,
segmented_indicatorIndex: indicator?.style.getPropertyValue("--segment-index"),
radio_selectedValue: selectedRadio?.getAttribute("value"),
textfield_value: tf.querySelector("input")?.value,
};
console.log("__elements_demo__", JSON.stringify(w.__results, null, 2));
});
});
});
9 changes: 9 additions & 0 deletions packages/elements/demo/seed-design/ui/action-button.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { SeedActionButton } from "@seed-design/elements";

/**
* Snippet (layer 2) — ActionButton needs no icon/label baked in, so this is just
* the class registered under its tag. Extend it here if you want project defaults.
*/
if (!customElements.get("seed-action-button")) {
customElements.define("seed-action-button", SeedActionButton);
}
5 changes: 5 additions & 0 deletions packages/elements/demo/seed-design/ui/badge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { SeedBadge } from "@seed-design/elements";

if (!customElements.get("seed-badge")) {
customElements.define("seed-badge", SeedBadge);
}
16 changes: 16 additions & 0 deletions packages/elements/demo/seed-design/ui/checkbox.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { SeedCheckbox } from "@seed-design/elements";

/**
* Snippet (layer 2) — the place to pin a concrete checkmark icon.
*
* DEFERRED: @karrotmarket/icon-data currently exposes icons as SVG *strings*
* only, which would force a raw `innerHTML` injection. We're holding the icon
* until icon-data provides them as SVG files (importable) or as icon web
* components — then set `this.checkedIcon` (or compose the icon element) here.
* Until then `<seed-checkbox>` renders without a checkmark glyph.
*/
export class Checkbox extends SeedCheckbox {}

if (!customElements.get("seed-checkbox")) {
customElements.define("seed-checkbox", Checkbox);
}
8 changes: 8 additions & 0 deletions packages/elements/demo/seed-design/ui/radio-group.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { SeedRadio, SeedRadioGroup } from "@seed-design/elements";

if (!customElements.get("seed-radio-group")) {
customElements.define("seed-radio-group", SeedRadioGroup);
}
if (!customElements.get("seed-radio")) {
customElements.define("seed-radio", SeedRadio);
}
8 changes: 8 additions & 0 deletions packages/elements/demo/seed-design/ui/segmented-control.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { SeedSegmentedControl, SeedSegmentedControlItem } from "@seed-design/elements";

if (!customElements.get("seed-segmented-control")) {
customElements.define("seed-segmented-control", SeedSegmentedControl);
}
if (!customElements.get("seed-segmented-control-item")) {
customElements.define("seed-segmented-control-item", SeedSegmentedControlItem);
}
5 changes: 5 additions & 0 deletions packages/elements/demo/seed-design/ui/skeleton.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { SeedSkeleton } from "@seed-design/elements";

if (!customElements.get("seed-skeleton")) {
customElements.define("seed-skeleton", SeedSkeleton);
}
5 changes: 5 additions & 0 deletions packages/elements/demo/seed-design/ui/switch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { SeedSwitch } from "@seed-design/elements";

if (!customElements.get("seed-switch")) {
customElements.define("seed-switch", SeedSwitch);
}
5 changes: 5 additions & 0 deletions packages/elements/demo/seed-design/ui/text-field.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { SeedTextField } from "@seed-design/elements";

if (!customElements.get("seed-text-field")) {
customElements.define("seed-text-field", SeedTextField);
}
5 changes: 5 additions & 0 deletions packages/elements/demo/seed-design/ui/text.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { SeedText } from "@seed-design/elements";

if (!customElements.get("seed-text")) {
customElements.define("seed-text", SeedText);
}
53 changes: 53 additions & 0 deletions packages/elements/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"name": "@seed-design/elements",
"version": "0.0.0",
"repository": {
"type": "git",
"url": "git+https://github.com/daangn/seed-design.git",
"directory": "packages/elements"
},
"sideEffects": [
"**/*.css",
"./lib/define/**",
"./src/define/**"
],
"type": "module",
"exports": {
".": {
"types": "./lib/index.d.ts",
"import": "./lib/index.js"
},
"./define/*": {
"types": "./lib/define/*.d.ts",
"import": "./lib/define/*.js"
},
"./package.json": "./package.json"
},
"files": [
"lib",
"src"
],
"scripts": {
"clean": "rm -rf lib",
"build": "vite build",
"lint:publish": "bun publint",
"test": "vitest run",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"peerDependencies": {
"@seed-design/css": ">=1.0.0 <3.0.0"
},
"dependencies": {
"lit": "^3.3.0"
},
"devDependencies": {
"@seed-design/css": "workspace:*",
"jsdom": "^29.0.2",
"vite": "^8.0.0",
"vite-plugin-dts": "^4.5.4",
"vitest": "^3.2.4"
},
"publishConfig": {
"access": "public"
}
}
Loading