Changes
13 changed files (+539/-2)
-
-
-
@@ -21,9 +21,11 @@ "default": "./dist/*.css"} }, "dependencies": { "@bufbuild/protobuf": "^2.2.2", "@fontsource-variable/inter": "^5.1.0", "@fontsource/ibm-plex-sans-jp": "^5.1.0", "@fontsource/monaspace-neon": "^5.1.0" "@fontsource/monaspace-neon": "^5.1.0", "@yamori/proto": "workspace:*" }, "devDependencies": { "astro": "^5.0.3",
-
-
-
@@ -93,6 +93,14 @@ <NavBarItem href="/components/button/">Button </NavBarItem> </NavBarGroup> <NavBarGroup title="Widgets"> <NavBarItem href="/widgets/workspace-list/"> Workspace List </NavBarItem> <NavBarItem href="/widgets/workspace-list-entry/"> Workspace List Entry </NavBarItem> </NavBarGroup> </NavBar> <main class="main"> <header class="header">
-
-
-
@@ -19,5 +19,14 @@ <p>ドメインデータには関与せず、インタラクションや表示といったユーザとのやりとりにのみ関心を持ちます。そのため Protobuf で定義されたデータを受け取る・出力することはありません。 </p> <h2>Widgets</h2> <p> CustomElement で実装されたドメインデータを表示する UI 要素です。 </p> <p> ドメインデータの入出力を行い、他のアプリケーションに流用できないような造りになっています。 Protobuf で定義されたメッセージの入出力がメインとなるため、 属性ではなくプロパティやメソッドがメインの I/F となります。 </p> </Document> </DocsLayout>
-
-
-
@@ -0,0 +1,71 @@--- // SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import DocsLayout from "../../layouts/DocsLayout.astro"; import Document from "../../components/Document.astro"; import Preview from "../../components/Preview.astro"; import ThemeMatrixPreview from "../../components/ThemeMatrixPreview.astro"; --- <script> import { create } from "@bufbuild/protobuf"; import { WorkspaceSchema } from "@yamori/proto/yamori/workspace/v1/workspace_pb.js"; import { OpenEvent, YamoriWorkspaceListEntry, } from "../../../widgets/workspace-list-entry/workspace-list-entry.ts"; customElements.whenDefined(YamoriWorkspaceListEntry.tagName).then(() => { for (const demo of document.querySelectorAll("[data-id=demo]")) { demo.setWorkspace( create(WorkspaceSchema, { id: { value: "ws-demo", }, displayName: "Demo", }), ); } for (const demo of document.querySelectorAll("[data-id=empty]")) { demo.setWorkspace( create(WorkspaceSchema, { id: { value: "ws-demo", }, displayName: "", }), ); } }); document.addEventListener("open", (event) => { if (event instanceof OpenEvent) { console.groupCollapsed("yamori-workspace-list-entry.open"); console.dir(event); console.groupEnd(); } }); </script> <DocsLayout title="Workspace List Entry"> <Document> <p> <a href="/widgets/workspace-list/"><code>yamori-workspace-list</code></a> 内で個別のワークスペースを表示する UI 要素です。 </p> <ThemeMatrixPreview> <yamori-workspace-list-entry data-id="demo" /> </ThemeMatrixPreview> <h2>Empty <code>display_name</code></h2> <p> <code>display_name</code> フィールドが空の場合は未設定表示となります。 </p> <ThemeMatrixPreview> <yamori-workspace-list-entry data-id="empty" /> </ThemeMatrixPreview> </Document> </DocsLayout>
-
-
-
@@ -0,0 +1,117 @@--- // SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import DocsLayout from "../../layouts/DocsLayout.astro"; import Document from "../../components/Document.astro"; import Preview from "../../components/Preview.astro"; import ThemeMatrixPreview from "../../components/ThemeMatrixPreview.astro"; import { create, toJsonString } from "@bufbuild/protobuf"; import { ListResponseSchema } from "@yamori/proto/yamori/workspace/v1/list_response_pb.js"; const sampleAttr = toJsonString( ListResponseSchema, create(ListResponseSchema, { result: { case: "ok", value: { workspaces: [ { id: { value: "ws-foo" }, displayName: "Foo", }, { id: { value: "ws-bar" }, displayName: "ばー", }, ], }, }, }), ); --- <script> import { create } from "@bufbuild/protobuf"; import { ListResponseSchema } from "@yamori/proto/yamori/workspace/v1/list_response_pb.js"; import { OpenEvent, YamoriWorkspaceList, } from "../../../widgets/workspace-list/workspace-list.ts"; customElements.whenDefined(YamoriWorkspaceList.tagName).then(() => { for (const demo of document.querySelectorAll("[data-id=demo]")) { demo.setListResponse( create(ListResponseSchema, { result: { case: "ok", value: { workspaces: [ { id: { value: "ws-foo" }, displayName: "Foo", }, { id: { value: "ws-bar" }, displayName: "ばー", }, ], }, }, }), ); } for (const demo of document.querySelectorAll("[data-id=system_error]")) { demo.setListResponse( create(ListResponseSchema, { result: { case: "systemError", value: { code: "SAMPLE_ERR", message: "サンプルエラー", }, }, }), ); } }); document.addEventListener("open", (event) => { if (event instanceof OpenEvent) { console.groupCollapsed("yamori-workspace-list.open (composed)"); console.dir(event); console.groupEnd(); } }); </script> <DocsLayout title="Workspace List"> <Document> <p> <code>yamori.workspace.v1.ListResponse</code> を表示する UI 要素です。 </p> <ThemeMatrixPreview> <yamori-workspace-list data-id="demo"></yamori-workspace-list> </ThemeMatrixPreview> <h2>System Error</h2> <p> レスポンスが <code>system_error</code> の場合はエラー表示となります。 </p> <ThemeMatrixPreview> <yamori-workspace-list data-id="system_error"></yamori-workspace-list> </ThemeMatrixPreview> <h2>Attribute Usage</h2> <p> <code>setListResponse</code> メソッドの代わりに <code>list-response</code> 属性に <code>toJsonString</code> の結果を値として指定することも可能です。 </p> <Preview> <yamori-workspace-list list-response={sampleAttr}></yamori-workspace-list> </Preview> </Document> </DocsLayout>
-
-
-
@@ -5,13 +5,16 @@ * SPDX-License-Identifier: AGPL-3.0-only*/ @import "@fontsource/monaspace-neon/400.css"; @import "@fontsource/monaspace-neon/600.css"; @import "@fontsource-variable/inter"; @import "@fontsource/ibm-plex-sans-jp/400.css"; @import "@fontsource/ibm-plex-sans-jp/600.css"; body { font-size: 1rem; line-height: 1.5; font-family: var(--font-sans); font-weight: var(--font-regular); background-color: oklch(var(--color-bg)); color: oklch(var(--color-fg) / var(--alpha-fg-medium));
-
-
-
@@ -3,8 +3,23 @@ // SPDX-License-Identifier: AGPL-3.0-onlyimport { YamoriButton } from "./components/button/button.ts"; import { YamoriWorkspaceList } from "./widgets/workspace-list/workspace-list.ts"; import { YamoriWorkspaceListEntry } from "./widgets/workspace-list-entry/workspace-list-entry.ts"; export { YamoriButton }; export { YamoriWorkspaceList, YamoriWorkspaceListEntry }; export function registerComponents(): void { YamoriButton.register(); } export function registerWidgets(): void { YamoriWorkspaceList.register(); YamoriWorkspaceListEntry.register(); } export function register(): void { YamoriButton.register(); registerComponents(); registerWidgets(); }
-
-
-
@@ -65,4 +65,7 @@ --font-lg: calc(var(--font-md) * var(--scale));--font-xl: calc(var(--font-lg) * var(--scale)); --font-sm: calc(var(--font-md) / var(--scale)); --font-xs: calc(var(--font-sm) / var(--scale)); --font-regular: 400; --font-bold: 600; }
-
-
-
@@ -0,0 +1,45 @@/* * SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> * SPDX-License-Identifier: AGPL-3.0-only */ :not(:is(style, script, :state(nonua), :--nonua, [nonua])) { all: unset; box-sizing: border-box; } :host { display: flex; flex-direction: column; padding: var(--space-px-8) var(--space-px-5); padding-bottom: var(--space-px-9); } :host(:is(:state(nodata), :--nodata, [nodata])) { display: none; } .label { display: block; font-family: var(--font-mono); font-size: var(--font-xs); color: oklch(var(--color-fg) / var(--alpha-fg-subtle)); text-transform: uppercase; } .display-name { display: block; font-size: var(--font-md); font-weight: var(--font-bold); margin-block-end: var(--space-px-6); color: oklch(var(--color-fg) / var(--alpha-fg-medium)); } .display-name:empty::before { content: "名称未設定"; text-decoration: oklch(var(--color-fg) / var(--alpha-fg-subtle)) wavy underline; text-decoration-thickness: 0.05rem; }
-
-
-
@@ -0,0 +1,93 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { type Workspace } from "@yamori/proto/yamori/workspace/v1/workspace_pb.js"; import { YamoriButton } from "../../components/button/button.ts"; import { wrapElement, YamoriElement } from "../../element.ts"; import css from "./workspace-list-entry.css?inline"; export class OpenEvent extends Event { constructor(public readonly workspace: Workspace) { super("open", { bubbles: true, cancelable: true, composed: true, }); } } export const YamoriWorkspaceListEntry = wrapElement( "yamori-workspace-list-entry", class extends YamoriElement { #shadow: ShadowRoot; #startMarker: Comment; #endMarker: Comment; #workspace: Workspace | null = null; constructor() { super(); this.#shadow = this.attachShadow({ mode: "open", }); const style = document.createElement("style"); style.textContent = css; this.#shadow.appendChild(style); this.#startMarker = document.createComment(""); this.#shadow.appendChild(this.#startMarker); this.#endMarker = document.createComment(""); this.#shadow.appendChild(this.#endMarker); this.internals.role = "listitem"; this.#render(); } #render = (): void => { const range = document.createRange(); range.setStartAfter(this.#startMarker); range.setEndBefore(this.#endMarker); range.deleteContents(); const workspace = this.#workspace; if (!workspace) { this.setCustomState("nodata"); return; } this.removeCustomState("nodata"); const label = document.createElement("span"); label.textContent = "Workspace"; label.classList.add("label"); this.#shadow.insertBefore(label, this.#endMarker); const displayName = document.createElement("span"); displayName.textContent = workspace.displayName; displayName.classList.add("display-name"); this.#shadow.insertBefore(displayName, this.#endMarker); const button = document.createElement(YamoriButton.tagName); button.textContent = "開く"; this.#shadow.insertBefore(button, this.#endMarker); button.addEventListener("click", (event) => { event.preventDefault(); event.stopPropagation(); this.dispatchEvent(new OpenEvent(workspace)); }); }; setWorkspace(workspace: Workspace) { this.#workspace = workspace; this.#render(); } }, );
-
-
-
@@ -0,0 +1,22 @@/* * SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> * SPDX-License-Identifier: AGPL-3.0-only */ :not(:is(style, script, :state(nonua), :--nonua, [nonua])) { all: unset; box-sizing: border-box; } :host { display: block; } .list { display: flex; flex-direction: column; } .list > :not(:last-child) { border-bottom: 1px solid oklch(var(--color-border) / var(--alpha-border-subtle)); }
-
-
-
@@ -0,0 +1,149 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { fromJsonString } from "@bufbuild/protobuf"; import { type ListResponse, ListResponseSchema, } from "@yamori/proto/yamori/workspace/v1/list_response_pb.js"; import { wrapElement, YamoriElement } from "../../element.ts"; import { YamoriWorkspaceListEntry } from "../workspace-list-entry/workspace-list-entry.ts"; import css from "./workspace-list.css?inline"; export { OpenEvent } from "../workspace-list-entry/workspace-list-entry.ts"; type ObservedAttributes = readonly ["pending", "list-response"]; export const YamoriWorkspaceList = wrapElement( "yamori-workspace-list", class extends YamoriElement { static get observedAttributes(): ObservedAttributes { return ["pending", "list-response"]; } #pending: boolean = this.hasAttribute("pending"); #data: ListResponse | null = this.#parseListResponseAttribute(); #shadow: ShadowRoot; #startMarker: Comment; #endMarker: Comment; attributeChangedCallback( name: ObservedAttributes[number], oldValue: string | null, newValue: string | null, ) { if (oldValue === newValue) { return; } switch (name) { case "pending": this.#pending = typeof newValue === "string"; this.#render(); return; case "list-response": this.#data = this.#parseListResponseAttribute(); this.#render(); return; } } #parseListResponseAttribute(): ListResponse | null { const value = this.getAttribute("list-response"); if (!value) { return null; } try { return fromJsonString(ListResponseSchema, value); } catch (error) { console.error(`The value of "list-response" attribute is invalid.`, error); return null; } } constructor() { super(); this.#shadow = this.attachShadow({ mode: "open", }); const style = document.createElement("style"); style.textContent = css; this.#shadow.appendChild(style); this.#startMarker = document.createComment(""); this.#shadow.appendChild(this.#startMarker); this.#endMarker = document.createComment(""); this.#shadow.appendChild(this.#endMarker); this.#render(); } setListResponse(listResponse: ListResponse): void { this.#data = listResponse; this.#render(); } #render(): void { const range = document.createRange(); range.setStartAfter(this.#startMarker); range.setEndBefore(this.#endMarker); range.deleteContents(); if (!this.#data) { // データがない場合はどうしようもないので読込中と同様とする const label = document.createElement("p"); label.textContent = "取得中..."; this.#shadow.insertBefore(label, this.#endMarker); return; } if (this.#pending) { const spinner = document.createElement("div"); spinner.classList.add("spinner"); this.#shadow.insertBefore(spinner, this.#endMarker); } switch (this.#data.result.case) { case "systemError": { const message = document.createElement("p"); message.textContent = `取得に失敗しました: ${this.#data.result.value.code}`; this.#shadow.insertBefore(message, this.#endMarker); return; } case "ok": { const list = document.createElement("ul"); list.classList.add("list"); this.#shadow.insertBefore(list, this.#endMarker); const { workspaces } = this.#data.result.value; customElements.whenDefined(YamoriWorkspaceListEntry.tagName).then(() => { for (const workspace of workspaces) { const item = document.createElement( YamoriWorkspaceListEntry.tagName, ) as InstanceType<typeof YamoriWorkspaceListEntry.constructor>; item.setWorkspace(workspace); list.appendChild(item); } }); return; } default: { const message = document.createElement("p"); message.textContent = "データが破損しています"; this.#shadow.insertBefore(message, this.#endMarker); return; } } } }, );
-