Changes
84 changed files (+515/-3484)
-
-
-
@@ -3,10 +3,6 @@ ## SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> # SPDX-License-Identifier: AGPL-3.0-only # What: ビルドされたドキュメントページとアセット。 # Why: 編集するものではないため。 /docs-dist # What: ビルドされたライブラリ JS 。 # Why: 編集するものではないため。 /dist
-
@@ -14,7 +10,3 @@# What: ビルドされた *.d.ts ファイルを格納するディレクトリ。 # Why: 自動生成されたディレクトリのため。 /types # What: Astro の中間ファイルとか。 # Why: 自動生成されたディレクトリのため。 .astro
-
-
packages/gui/REUSE.toml (deleted)
-
@@ -1,13 +0,0 @@# このディレクトリ配下のファイルを reuse-tool が扱う際の設定。 # # SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> # SPDX-License-Identifier: AGPL-3.0-only version = 1 # このパッケージの HTML はドキュメント用途であり、且つ複数ページあるため # いちいち `*.html.license` ファイルを作成するのは非常に邪魔なのでまとめて指定。 [[annotations]] path = "src/**/*.html" SPDX-FileCopyrightText = "2024 Shota FUJI <pockawoooh@gmail.com>" SPDX-License-Identifier = "AGPL-3.0-only"
-
-
packages/gui/astro.config.js (deleted)
-
@@ -1,15 +0,0 @@// [Astro](https://astro.build/) のビルド設定。 // // SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { defineConfig } from "astro/config"; export default defineConfig({ trailingSlash: "always", srcDir: "./src/docs", outDir: "./docs-dist", devToolbar: { enabled: false, }, });
-
-
packages/gui/package.json (deleted)
-
@@ -1,38 +0,0 @@{ "name": "@yamori/gui", "private": true, "type": "module", "scripts": { "dev": "ASTRO_TELEMETRY_DISABLED=1 astro dev", "build:lib": "vite build && tsc -p tsconfig.build.jsonc", "build:docs": "ASTRO_TELEMETRY_DISABLED=1 astro build", "build:all": "bun build:lib && bun build:docs", "build": "bun build:lib", "check": "tsc", "clean": "rm -rf dist docs-dist types", "prepare": "bun build:lib" }, "exports": { ".": { "types": "./types/lib.d.ts", "default": "./dist/lib.js" }, "./*.css": { "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", "@yamori/proto": "workspace:*", "lit-html": "^3.2.1", "lucide": "^0.468.0" }, "devDependencies": { "astro": "^5.0.3", "typescript": "^5.7.2", "vite": "^6.0.2" } }
-
-
-
packages/gui/src/all.css (deleted)
-
@@ -1,16 +0,0 @@/* @yamori/gui の全てのスタイル。 * JS でテーマを動的に変える場合は個別に CSS を読み込むこと。 * * SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> * SPDX-License-Identifier: AGPL-3.0-only */ @import "./reset.css"; @import "./vars/base.css"; @import "./vars/dark.css" (prefers-color-scheme: dark); @import "./vars/contrast.css" (prefers-contrast: more); @import "./vars/dark-contrast.css" (prefers-color-scheme: dark) and (prefers-contrast: more); @import "./global.css";
-
-
-
@@ -1,114 +0,0 @@/* * SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> * SPDX-License-Identifier: AGPL-3.0-only */ :not(:is(style, script)) { all: unset; box-sizing: border-box; } :host { display: flex; position: relative; padding: var(--space-px-2); border: 1px solid oklch(var(--color-border) / var(--alpha-border-medium)); background-image: linear-gradient( to right bottom, oklch(100% 0% 0deg / 30%), oklch(0% 0% 0deg / 5%) ); background-color: oklch(var(--color-bg)); border-radius: 5px; color: oklch(var(--color-fg) / var(--alpha-fg-medium)); user-select: none; cursor: pointer; } :host([inline]) { display: inline-flex; } :host(:is(:disabled, [aria-disabled="true"])) { color: oklch(var(--color-fg) / var(--alpha-fg-subtle)); cursor: not-allowed; text-decoration: line-through; } :host(:is(:state(pending), [x--_pending])) { color: transparent; } @media (hover: hover) { :host(:not(:is(:disabled, [aria-disabled="true"], :state(pending), [x--_pending])):hover) { border-color: oklch(var(--color-border) / var(--alpha-border-strong)); } } :host(:focus-visible:focus-visible), :host(:has(:focus-visible:focus-visible)) { border-color: oklch(var(--color-focus)); box-shadow: 0 0 0 var(--size-focus-ring) oklch(var(--color-focus) / var(--alpha-focus-ring)); outline: none; } :host(:enabled:not(:is([aria-disabled="true"], :state(pending), [x--_pending])):is(:active, :state(pressed), [x--_pressed])) { background-image: linear-gradient( to left top, oklch(100% 0% 0deg / 30%), oklch(0% 0% 0deg / 5%) ); } .inner { flex: 1; padding: var(--space-px-3) var(--space-px-5); border: 1px solid oklch(var(--color-fg) / 5%); background-image: linear-gradient( to left top, oklch(100% 0% 0deg / 10%), oklch(0% 0% 0deg / 3%) ); background-color: inherit; border-radius: inherit; text-align: center; } :host(:not(:is(:disabled, [aria-disabled="true"], :state(pending), [x--_pending])):is(:active, :state(pressed), [x--_pressed])) .inner { transform: translateY(1px); filter: brightness(0.99); } .spinner { display: none; position: absolute; top: 0; right: 0; bottom: 0; left: 0; margin: auto; width: var(--font-md); height: var(--font-md); border: 2px solid oklch(var(--color-fg) / var(--alpha-fg-medium)); border-top-color: transparent; border-radius: 50%; } @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } :host(:is(:state(pending), [x--_pending])) .spinner { display: block; animation: 1.3s 0s forwards infinite ease-in-out spin; }
-
-
-
@@ -1,157 +0,0 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { wrapElement, YamoriElement } from "../../element.ts"; import css from "./button.css?inline"; type ObservedAttributes = readonly ["pending"]; export const YamoriButton = wrapElement({ tagName: "yamori-button", constructor: class extends YamoriElement { static formAssociated = true; static get observedAttributes(): ObservedAttributes { return ["pending"]; } #label: HTMLSpanElement; #spinner: HTMLDivElement; #pending: boolean = this.hasAttribute("pending"); attributeChangedCallback( name: ObservedAttributes[number], oldValue: string | null, newValue: string | null, ) { if (oldValue === newValue) { return; } switch (name) { case "pending": this.setPending(typeof newValue === "string"); return; } } setPending(value: boolean) { this.#pending = value; if (value) { this.setCustomState("pending"); // NOTE: `aria-busy` は Live Region が更新中かどうかのフラグであり、 // ボタンといった内容の変わらないトリガーの状態表示としては不適切 // であるため弄らない。 this.internals.ariaDisabled = "true"; } else { this.removeCustomState("pending"); this.internals.ariaDisabled = "false"; } } constructor() { super(); const shadow = this.attachShadow({ mode: "open", }); const style = document.createElement("style"); style.textContent = css; shadow.appendChild(style); this.#label = document.createElement("span"); this.#label.classList.add("inner"); shadow.appendChild(this.#label); const slot = document.createElement("slot"); this.#label.appendChild(slot); this.#spinner = document.createElement("div"); this.#spinner.classList.add("spinner"); shadow.appendChild(this.#spinner); this.internals.role = "button"; } override connectedCallback() { super.connectedCallback(); // NOTE: 現状属性に影響を与えずに `tabIndex` を変える方法がない。 // 属性を変える操作は DOM に接続してから行わないと DOM のエラー // となるためここで行う必要がある。 this.tabIndex = 0; this.addEventListener("click", this.#onClick, { capture: true, }); this.addEventListener("keydown", this.#onKeyDown); this.addEventListener("keyup", this.#onKeyUp); } disconnectedCallback() { this.removeEventListener("click", this.#onClick, { capture: true, }); this.removeEventListener("keydown", this.#onKeyDown); this.removeEventListener("keyup", this.#onKeyUp); } get disabled(): boolean { return this.hasAttribute("disabled"); } #isEnabled = (): boolean => { return ( !this.#pending && !this.hasAttribute("disabled") && this.ariaDisabled !== "true" ); }; #onClick = (event: MouseEvent): void => { if (!this.#isEnabled()) { event.preventDefault(); event.stopPropagation(); return; } }; #onKeyDown = (event: KeyboardEvent): void => { if (event.key !== " ") { return; } if (!this.#isEnabled()) { event.preventDefault(); return; } this.setCustomState("pressed"); }; #onKeyUp = (event: KeyboardEvent): void => { if (event.key !== " ") { return; } if (!this.#isEnabled()) { event.preventDefault(); return; } this.removeCustomState("pressed"); // `clientX/Y` は `getBoundingClientRect()` を使うことで計算できるが、 // レイアウトにアクセスするためコストがかかる。現状これらのプロパティは // 利用していないためスキップしている。 this.dispatchEvent( new MouseEvent("click", { view: event.view, bubbles: true, cancelable: true, }), ); }; }, });
-
-
-
@@ -1,50 +0,0 @@/* * SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> * SPDX-License-Identifier: AGPL-3.0-only */ :not(:is(style, script)) { all: unset; box-sizing: border-box; } :host { display: flex; flex-direction: column; border: 1px solid oklch(var(--color-border) / var(--alpha-border-medium)); background-color: oklch(var(--color-bg)); border-radius: var(--space-px-2); box-shadow: 2px 2px 4px 0 oklch(0% 0% 0deg / 5%); overflow: hidden; } .title { display: block; font-size: var(--font-sm); font-weight: var(--font-bold); padding: var(--space-px-2) var(--space-px-5); color: oklch(var(--color-fg) / var(--alpha-fg-subtle)); } .body { display: flex; flex-direction: column; gap: var(--space-px-8); padding: var(--space-px-4); } .message { display: block; } .actions { display: flex; justify-content: flex-end; gap: var(--space-px-6); } :host(:not(:has([slot="action"]))) .actions { display: none; }
-
-
-
@@ -1,45 +0,0 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { wrapElement, YamoriElement } from "../../element.ts"; import css from "./callout.css?inline"; export const YamoriCallout = wrapElement({ tagName: "yamori-callout", constructor: class extends YamoriElement { constructor() { super(); const shadow = this.attachShadow({ mode: "open", }); const style = document.createElement("style"); style.textContent = css; shadow.appendChild(style); const titleSlot = document.createElement("slot"); titleSlot.name = "title"; titleSlot.classList.add("title"); shadow.appendChild(titleSlot); const body = document.createElement("div"); body.classList.add("body"); shadow.appendChild(body); const messageSlot = document.createElement("slot"); messageSlot.classList.add("message"); body.appendChild(messageSlot); const actions = document.createElement("slot"); actions.name = "action"; actions.classList.add("actions"); body.appendChild(actions); } override connectedCallback(): void { super.connectedCallback(); } }, });
-
-
-
@@ -1,51 +0,0 @@/* * SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> * SPDX-License-Identifier: AGPL-3.0-only */ :not(:is(style, script)) { all: unset; box-sizing: border-box; } :host { display: flex; flex-direction: column; gap: var(--space-px-6); align-items: center; } .icon { display: flex; font-size: var(--space-px-12); margin-block-end: var(--space-px-8); } :host(:not(:has([slot="icon"]))) .icon { display: none; } .title { font-size: var(--font-lg); font-weight: var(--font-bold); color: oklch(var(--color-fg) / var(--alpha-fg-strong)); } :host(:not(:has([slot="title"]))) .title { display: none; } .actions { display: flex; flex-direction: column; align-items: stretch; gap: var(--space-px-6); min-width: 0; width: 100%; max-width: 20rem; margin-block-start: var(--space-px-10); } :host(:not(:has([slot="action"]))) .actions { display: none; }
-
-
-
@@ -1,45 +0,0 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { wrapElement, YamoriElement } from "../../element.ts"; import css from "./empty.css?inline"; export const YamoriEmpty = wrapElement({ tagName: "yamori-empty", constructor: class extends YamoriElement { constructor() { super(); const shadow = this.attachShadow({ mode: "open", }); const style = document.createElement("style"); style.textContent = css; shadow.appendChild(style); const icon = document.createElement("slot"); icon.name = "icon"; icon.classList.add("icon"); shadow.appendChild(icon); const title = document.createElement("slot"); title.name = "title"; title.classList.add("title"); shadow.appendChild(title); const body = document.createElement("slot"); shadow.appendChild(body); const actions = document.createElement("slot"); actions.name = "action"; actions.classList.add("actions"); shadow.appendChild(actions); } override connectedCallback() { super.connectedCallback(); } }, });
-
-
-
@@ -1,56 +0,0 @@/* * SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> * SPDX-License-Identifier: AGPL-3.0-only */ :not(:is(style, script)) { all: unset; box-sizing: border-box; } :host { display: block; width: 100cqw; height: 100cqh; box-sizing: border-box; overflow-y: auto; } .identity, .contents { padding: var(--space-px-5) var(--space-px-7); } .identity { display: block; font-weight: var(--font-bold); font-size: var(--font-sm); color: oklch(var(--color-fg) / var(--alpha-fg-strong)); } .contents { display: flex; flex-direction: column; margin-block-start: var(--space-px-10); } @container (min-width: 80rem) { :host { display: grid; grid-template-columns: minmax(0, 1fr) 40rem; gap: var(--space-px-4); padding: var(--space-px-8); } .identity { place-self: center; } .contents { grid-column: 2 / 3; align-self: center; justify-self: stretch; margin-block-start: 0; } }
-
-
-
@@ -1,37 +0,0 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { html, render } from "lit-html"; import { wrapElement, YamoriElement } from "../../element.ts"; import css from "./single-section-layout.css?inline"; export const YamoriSingleSectionLayout = wrapElement({ tagName: "yamori-single-section-layout", constructor: class extends YamoriElement { constructor() { super(); const shadow = this.attachShadow({ mode: "open" }); render( html` <style>${css}</style> <div class="identity"> <!-- TODO: Icon logo --> Yamori </div> <slot class="contents"></slot> `, shadow, ); } override connectedCallback() { super.connectedCallback(); } }, });
-
-
-
@@ -1,59 +0,0 @@--- // SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only --- <style> .doc > :global(:where(*)) { display: block; margin: 1em 0; } .doc :global(h2) { font-weight: bold; font-size: var(--font-lg); margin-top: 2em; } .doc :global(a) { cursor: pointer; text-decoration: underline; } .doc :global(ul) { display: flex; flex-direction: column; align-items: start; padding-inline-start: 1.25em; } .doc :global(ul > li) { display: list-item; list-style: disc; } .doc :global(code) { font-family: var(--font-mono); text-decoration: inherit; } .doc :global(> :is(p, ul, ol) code) { display: inline-block; font-size: var(--font-sm); padding: calc((var(--font-md) - var(--font-sm)) * 1.5 / 2 - 2px) calc(var(--font-sm) * 0.5); border: 1px solid oklch(var(--color-border) / var(--alpha-border-subtle)); background-color: oklch(var(--color-fg) / 2%); border-radius: 2px; vertical-align: middle; } .doc :global(strong) { font-weight: var(--font-bold); } </style> <div class="doc"> <slot /> </div>
-
-
-
@@ -1,23 +0,0 @@--- // SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only const { class: className, ...rest } = Astro.props; --- <style> .nav { padding: var(--space-px-4) var(--space-px-5); } .list { display: flex; flex-direction: column; } </style> <nav {...rest} class:list={["nav", className]}> <ul class="list"> <slot /> </ul> </nav>
-
-
-
@@ -1,68 +0,0 @@--- // SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only const { defaultOpened = true, title } = Astro.props; --- <style> .wrapper { display: block; margin: var(--space-px-6) 0; } .collapsible { display: block; min-width: 0; width: 100%; } .toggle { display: flex; justify-content: space-between; align-items: center; min-width: 0; width: 100%; border: 1px solid transparent; padding: var(--space-px-4) var(--space-px-5); font-size: var(--font-sm); font-weight: bold; border-radius: 4px; cursor: pointer; } .toggle:hover { background-color: oklch(var(--color-fg) / 5%); } .toggle:focus-visible { border-color: oklch(var(--color-border) / var(--alpha-border-strong)); } .toggle-indicator::before { content: "+"; } .collapsible[open] .toggle-indicator::before { content: "-"; } .list { display: flex; flex-direction: column; padding-inline-start: var(--space-px-6); } </style> <li class="wrapper"> <details class="collapsible" open={defaultOpened}> <summary class="toggle"> <span>{title}</span> <span class="toggle-indicator" /> </summary> <ul class="list"> <slot /> </ul> </details> </li>
-
-
-
@@ -1,38 +0,0 @@--- // SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only const { href } = Astro.props; --- <style> .wrapper { display: block; margin: var(--space-px-3) 0; } .link { display: block; min-width: 0; width: 100%; padding: var(--space-px-4) var(--space-px-5); border: 1px solid transparent; border-radius: 4px; cursor: pointer; } .link:hover { background-color: oklch(var(--color-fg) / 5%); } .link:focus-visible { border-color: oklch(var(--color-border) / var(--alpha-border-strong)); } </style> <li class="wrapper"> <a class="link" href={href}> <slot /> </a> </li>
-
-
-
@@ -1,317 +0,0 @@--- // SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import ThemeOverride from "./ThemeOverride.astro"; interface Props { title?: string; colorScheme?: "dark" | "light"; contrast?: "more" | "no-preference"; } const { colorScheme, contrast, size, title, padding, ...rest } = Astro.props; const style = size ? `--_width:${size[0]}px;--_height:${size[1]}px` : undefined; const canvasStyle = size ? `aspect-ratio:${size[0]} / ${size[1]}` : undefined; --- <style> .preview { display: flex; flex-direction: column; border: 1px solid oklch(var(--color-border) / var(--alpha-border-medium)); border-radius: 4px; overflow: hidden; } .preview.fullscreen { position: fixed; inset: 0; margin: 0; border: none; background-color: oklch(var(--color-bg)); border-radius: 0; z-index: 999; } .canvas { --_grid-color-value: oklch( var(--color-border) / var(--alpha-border-subtle) ); flex: 1; display: block; box-sizing: border-box; } .preview.fullscreen > .canvas { min-height: 0; height: auto !important; flex-basis: 100% !important; background-image: linear-gradient( var(--_grid-color-value) 0.5px, transparent 0.5px, transparent calc(100% - 0.5px), var(--_grid-color-value) calc(100% - 0.5px) ), linear-gradient( 90deg, var(--_grid-color-value) 0.5px, transparent 0.5px, transparent calc(100% - 0.5px), var(--_grid-color-value) calc(100% - 0.5px) ); background-size: var(--space-px-12) var(--space-px-12); } .preview.fullscreen > .canvas:has(.override.container) { position: relative; aspect-ratio: auto !important; overflow: auto; } .override { display: block; font-size: 1rem; line-height: 1.5; font-family: var(--font-sans); font-weight: var(--font-regular); box-sizing: border-box; background-color: oklch(var(--color-bg)); color: oklch(var(--color-fg) / var(--alpha-fg-medium)); } .override.padded { padding: var(--space-px-7); } .override.container { container-type: size; width: var(--_width); height: var(--_height); transform-origin: top left; } .preview.fullscreen .override:not(.container) { width: 100%; height: 100%; } .preview.fullscreen .override.container { position: absolute; top: 50%; left: 50%; box-shadow: var(--space-px-1) var(--space-px-1) var(--space-px-6) oklch(0% 0% 0deg / 15%); transform: translate(-50%, -50%) !important; } .header { display: block; padding: var(--space-px-5); font-size: var(--font-sm); font-weight: bold; border-bottom: 1px solid oklch(var(--color-border) / var(--alpha-border-subtle)); background-color: oklch(var(--color-fg) / 2%); color: oklch(var(--color-fg) / var(--alpha-fg-medium)); } .footer { display: flex; justify-content: space-between; align-items: center; gap: var(--space-px-3); border-top: 1px solid oklch(var(--color-border) / var(--alpha-border-subtle)); font-family: var(--font-mono); font-size: var(--font-xs); padding: var(--space-px-4); background-color: oklch(var(--color-fg) / 2%); color: oklch(var(--color-fg) / var(--alpha-fg-subtle)); } .fullscreen-button { flex-shrink: 0; display: flex; justify-content: center; align-items: center; width: var(--space-px-9); height: var(--space-px-9); cursor: pointer; } .fullscreen-button:focus-visible { color: oklch(var(--color-focus)); } .fullscreen-button-icon { rotate: 45deg; } .fullscreen-button-label { opacity: 0; position: absolute; top: 0; left: 0; width: 1px; height: 1px; overflow: hidden; white-space: nowrap; } .footer > code { flex-shrink: 1; } </style> <script> let fullscreenTarget = null; document.addEventListener("keydown", (event) => { if ( event.isComposing || event.key !== "Escape" || !fullscreenTarget || !(fullscreenTarget instanceof HTMLButtonElement) ) { return; } event.preventDefault(); event.stopPropagation(); fullscreenTarget.click(); }); function toggleFullscreen({ container, trigger, icon }) { if (container.classList.contains("fullscreen")) { container.classList.remove("fullscreen"); icon.textContent = "←→"; trigger.title = "全画面表示"; fullscreenTarget = null; } else { container.classList.add("fullscreen"); icon.textContent = "→←"; trigger.title = "全画面表示をやめる"; fullscreenTarget = trigger; } } for (const button of document.getElementsByClassName("fullscreen-button")) { button.addEventListener("click", (event) => { event.preventDefault(); event.stopPropagation(); const container = button.parentElement?.parentElement; if (!container || !container.classList.contains("preview")) { return; } const icon = button.querySelector(".fullscreen-button-icon"); if (typeof document.startViewTransition === "function") { container.style.viewTransitionName = "preview"; const transition = document.startViewTransition(() => { toggleFullscreen({ container, trigger: button, icon, }); }); transition.finished.then(() => { container.style.viewTransitionName = ""; }); } else { toggleFullscreen({ container, trigger: button, icon, }); } }); } function resizeOverride(el) { if (!el) { return; } const width = parseInt(el.getAttribute("data-width"), 10); const height = parseInt(el.getAttribute("data-height"), 10); const rect = el.getBoundingClientRect(); const parent = el.parentElement.getBoundingClientRect(); const scale = parent.width / width; el.style.transform = `scale(${scale})`; const canvasHeight = height * scale; el.parentElement.style.flexBasis = `${canvasHeight}px`; el.parentElement.style.height = `${canvasHeight}px`; } const observer = new ResizeObserver((entries) => { for (const entry of entries) { resizeOverride( entry.target.querySelector("docs-theme-override.override.container"), ); } }); for (const container of document.querySelectorAll( "docs-theme-override.override.container", )) { resizeOverride(container); observer.observe(container.parentElement.parentElement); } </script> <div {...rest} class="preview"> { title && ( <div class="header"> {title} </div> ) } <div class="canvas" style={canvasStyle}> <ThemeOverride class:list={["override", { padded: padding !== "no", container: !!size }]} color-scheme={colorScheme} contrast={contrast} style={style} data-width={size?.[0]} data-height={size?.[1]} > <slot /> </ThemeOverride> </div> <div class="footer"> <button class="fullscreen-button" title="全画面表示"> <span class="fullscreen-button-icon">←→</span> </button> { (colorScheme || contrast) && ( <code> {colorScheme && <span>(prefers-color-scheme: {colorScheme})</span>} {colorScheme && contrast ? <span> and </span> : null} {contrast && <span>(prefers-contrast: {contrast})</span>} </code> ) } </div> </div>
-
-
-
@@ -1,47 +0,0 @@--- // SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import Preview from "./Preview.astro"; const colorSchemes = ["light", "dark"]; const contrasts = ["no-preference", "more"]; const matrix = colorSchemes .map((colorScheme) => { return contrasts.map((contrast) => { return { colorScheme, contrast, }; }); }) .flat(); --- <style> .matrix { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(20rem, 100%), 1fr)); gap: var(--space-px-8); } .preview { width: 100%; } </style> <div class="matrix"> { matrix.map((overrides) => ( <Preview title={Astro.props.title} padding={Astro.props.padding} size={Astro.props.size} {...overrides} > <slot /> </Preview> )) } </div>
-
-
-
@@ -1,14 +0,0 @@--- // SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only --- <script> import { DocsThemeOverride } from "./docs-theme-override.ts"; DocsThemeOverride.register(); </script> <docs-theme-override {...Astro.props}> <slot /> </docs-theme-override>
-
-
-
@@ -1,135 +0,0 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { wrapElement, YamoriElement } from "../../element.ts"; import base from "../../vars/base.css?inline"; import dark from "../../vars/dark.css?inline"; import contrast from "../../vars/contrast.css?inline"; import darkContrast from "../../vars/dark-contrast.css?inline"; // NOTE: Safari (おそらく WebKit) はバグによってスタイルの `media` 属性が反映されない。 // 開発者ツールで属性値にスペースを足すなどして手動で編集すれば反映されるが、 // JS で変更した値は `<style>` `<link>` 共に無視される。 // <https://bugs.webkit.org/show_bug.cgi?id=26179> // 上記バグレポートのアタッチメントは Safari 18.2 で確認したところ解決している。 // CustomElement 固有か Shadow DOM 固有かはわからないが根本解決はされていないため // Safari 上でテーマ固定・マトリクスを使った確認はできない。 export const DocsThemeOverride = wrapElement({ tagName: "docs-theme-override", constructor: class DocsThemeOverride extends YamoriElement { static get observedAttributes() { return ["color-scheme", "contrast"] as const; } #contrast: "more" | "no-preference" | null = null; #colorScheme: "dark" | "light" | null = null; #darkStyle: HTMLStyleElement; #contrastStyle: HTMLStyleElement; #darkContrastStyle: HTMLStyleElement; attributeChangedCallback( name: (typeof DocsThemeOverride)["observedAttributes"][number], oldValue: string | null, newValue: string | null, ) { if (oldValue === newValue) { return; } switch (name) { case "color-scheme": this.setColorScheme(newValue); return; case "contrast": this.setContrast(newValue); return; } } setContrast(value: string | null): void { switch (value) { case null: case "more": case "no-preference": this.#contrast = value; this.updateMediaQueries(); return; default: console.warn( `Invalid contrast override "${value}". Accepted values are: "more", "no-preference" and null`, ); return; } } setColorScheme(value: string | null): void { switch (value) { case null: case "dark": case "light": this.#colorScheme = value; this.updateMediaQueries(); return; default: console.warn( `Invalid color-scheme override "${value}". Accepted values are: "dark", "light" and null`, ); return; } } updateMediaQueries(): void { const dark = this.#colorScheme === null ? "(prefers-color-scheme: dark)" : this.#colorScheme === "dark" ? "all" : "not all"; const contrast = this.#contrast === null ? "(prefers-contrast: more)" : this.#contrast === "more" ? "all" : "not all"; this.#darkStyle.media = dark; this.#contrastStyle.media = contrast; this.#darkContrastStyle.media = dark === contrast ? dark : `${dark} and ${contrast}`; } constructor() { super(); const shadow = this.attachShadow({ mode: "open", }); const baseStyle = document.createElement("style"); baseStyle.textContent = base; shadow.appendChild(baseStyle); this.#darkStyle = document.createElement("style"); this.#darkStyle.textContent = dark; shadow.appendChild(this.#darkStyle); this.#contrastStyle = document.createElement("style"); this.#contrastStyle.textContent = contrast; shadow.appendChild(this.#contrastStyle); this.#darkContrastStyle = document.createElement("style"); this.#darkContrastStyle.textContent = darkContrast; shadow.appendChild(this.#darkContrastStyle); this.updateMediaQueries(); const slot = document.createElement("slot"); shadow.appendChild(slot); } }, });
-
-
-
@@ -1,130 +0,0 @@--- // SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import "../../all.css"; import NavBar from "../components/NavBar.astro"; import NavBarGroup from "../components/NavBarGroup.astro"; import NavBarItem from "../components/NavBarItem.astro"; const { title } = Astro.props; --- <html lang="ja"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>{title} | Yamori GUI</title> <style> .body { position: absolute; inset: 0; display: grid; grid-template-columns: minmax(0, 1fr); grid-template-rows: max-content minmax(0, 1fr); gap: var(--space-px-12); } .nav { border-bottom: 1px solid oklch(var(--color-border) / var(--alpha-border-medium)); } @media (min-width: 1200px) { .body { grid-template-columns: minmax(0, 1fr) minmax(0, 60rem); grid-template-rows: 100%; } .nav { position: sticky; top: 0; border-bottom: none; border-right: 1px solid oklch(var(--color-border) / var(--alpha-border-medium)); } } @media (min-width: 1700px) { .body { grid-template-columns: minmax(0, 1fr) minmax(0, 60rem) minmax(0, 1fr); } } .main { display: block; padding: var(--space-px-4); width: 100%; overflow-y: auto; } .header { display: block; padding: var(--space-px-9) var(--space-px-8); border-bottom: 1px solid oklch(var(--color-border) / var(--alpha-border-subtle)); color: oklch(var(--color-fg) / var(--alpha-fg-strong)); } .title { font-size: var(--font-xl); font-weight: bold; } .contents { display: block; margin: var(--space-px-10) var(--space-px-8); } </style> </head> <body class="body"> <NavBar class="nav"> <NavBarItem href="/"> About </NavBarItem> <NavBarGroup title="Styles"> <NavBarItem href="/styles/globals/"> Globals </NavBarItem> <NavBarItem href="/styles/spacings/"> Spacings </NavBarItem> </NavBarGroup> <NavBarGroup title="Components"> <NavBarItem href="/components/button/"> Button </NavBarItem> <NavBarItem href="/components/callout/"> Callout </NavBarItem> <NavBarItem href="/components/empty/"> Empty </NavBarItem> <NavBarItem href="/components/single-section-layout/"> Single Section Layout </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"> <h1 class="title">{title}</h1> </header> <div class="contents"> <slot /> </div> </main> </body> </html>
-
-
-
@@ -1,82 +0,0 @@--- // 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 { YamoriButton } from "../../../components/button/button.ts"; YamoriButton.register(); const buttons = document.querySelectorAll("yamori-button"); buttons.forEach((button) => { button.addEventListener("click", (event) => { console.log(event); }); }); </script> <DocsLayout title="Button"> <Document> <p> ユーザがクリックやタップをすることでアクションを実行する UI です。 以下の操作を「押す」という操作とみなします。 </p> <ul> <li>クリック (マウス)</li> <li>タップ (タッチインターフェイス)</li> <li>フォーカスが当たっている状態で <kbd>Space</kbd> キー (キーボード)</li> </ul> <ThemeMatrixPreview> <yamori-button>ボタン</yamori-button> </ThemeMatrixPreview> <h2>Inline / Block</h2> <p> ボタンはデフォルトでブロックレベル要素となっています。 <code>inline</code> 属性を指定することでインラインになります。 </p> <Preview title="inline"> <yamori-button inline="">ボタン</yamori-button> </Preview> <Preview title="block (デフォルト)"> <yamori-button>Button</yamori-button> </Preview> <h2>Disabled</h2> <p> <code>yamori-button</code> は form-associated custom element であるため、 <code>disabled</code> 属性がそのまま使えます。 </p> <ThemeMatrixPreview> <yamori-button disabled>ボタン</yamori-button> </ThemeMatrixPreview> <p> ただ、<code>disabled</code> 属性はフォーカスができないというアクセシビリティにおける大きな欠陥仕様があります。 <code>aria-disabled</code> 属性を可能な限り使いましょう。 </p> <Preview title="aria-disabled"> <yamori-button aria-disabled="true">ボタン</yamori-button> </Preview> <h2>Pending</h2> <p> ボタンの押下に伴うアクションの実行中やアクションに必要な準備中でボタンが押せない場合は、 <code>pending</code> 属性を指定することで読込中表示にすることができます。 </p> <ThemeMatrixPreview> <yamori-button pending="">ボタン</yamori-button> </ThemeMatrixPreview> <p> ユーザインタラクションに起因する読込中表示の場合は明確ですが、そうでない場合は「どうして読込中表示なのか」「何を読み込んでいるのか」が分かりづらくなりがちです。可能な限り <code>title</code> 属性や補助テキストを使って状況を説明しましょう。 </p> </Document> </DocsLayout>
-
-
-
@@ -1,45 +0,0 @@--- // 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 { YamoriButton } from "../../../components/button/button.ts"; import { YamoriCallout } from "../../../components/callout/callout.ts"; YamoriCallout.register(); YamoriButton.register(); </script> <DocsLayout title="Callout"> <Document> <p> エラーや警告といったページ内要素に関連する重要な情報を表示する UI です。 </p> <ThemeMatrixPreview> <yamori-callout> <span slot="title">タイトル</span> メッセージ。 <yamori-button slot="action" inline>操作1</yamori-button> <yamori-button slot="action" inline>操作2</yamori-button> </yamori-callout> </ThemeMatrixPreview> <p> <code>action</code> スロットは未指定とすることができます。 </p> <Preview title="No named slots"> <yamori-callout> <span slot="title">タイトル</span> メッセージ。 </yamori-callout> </Preview> </Document> </DocsLayout>
-
-
-
@@ -1,40 +0,0 @@--- // 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 { YamoriButton } from "../../../components/button/button.ts"; import { YamoriEmpty } from "../../../components/empty/empty.ts"; import { LucideBadgeInfo } from "../../../lucide/icons.ts"; YamoriButton.register(); YamoriEmpty.register(); LucideBadgeInfo.register(); </script> <DocsLayout title="Empty"> <Document> <p> データがまだ存在しない場合や取得に失敗した場合など、ページやセクションの主たるコンテンツが空の場合に表示する UI です。 </p> <ThemeMatrixPreview title="Demo"> <yamori-empty> <lucide-badge-info slot="icon" /> <span slot="title">データがありません</span> データを追加しましょう。 <yamori-button slot="action">追加</yamori-button> <yamori-button slot="action">閉じる</yamori-button> </yamori-empty> </ThemeMatrixPreview> </Document> </DocsLayout>
-
-
-
@@ -1,34 +0,0 @@--- // 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 { YamoriSingleSectionLayout } from "../../../components/single-section-layout/single-section-layout.ts"; YamoriSingleSectionLayout.register(); </script> <DocsLayout title="Button"> <Document> <p> ログインページやスプラッシュスクリーンのように、ユーザのとれるアクションが 1~2 に限定された状況で表示する UI レイアウト。 </p> <ThemeMatrixPreview title="320x500" padding="no" size={[320, 500]}> <yamori-single-section-layout> CONTENTS </yamori-single-section-layout> </ThemeMatrixPreview> <Preview title="1280x720" padding="no" size={[1280, 720]}> <yamori-single-section-layout> CONTENTS </yamori-single-section-layout> </Preview> </Document> </DocsLayout>
-
-
packages/gui/src/docs/pages/index.astro (deleted)
-
@@ -1,40 +0,0 @@--- // 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"; --- <DocsLayout title="Yamori GUI"> <Document> <p> Yamori のスタイルシートとコンポーネントのドキュメント兼デモサイトです。 </p> <h2>Styles</h2> <p> Yamori のスタイルシートが定義するグローバルな影響と CSS カスタムプロパティです。 </p> <h2>Components</h2> <p> CustomElement で実装された汎用的な UI 要素です。 </p> <p> ドメインデータには関与せず、インタラクションや表示といったユーザとのやりとりにのみ関心を持ちます。そのため Protobuf で定義されたデータを受け取る・出力することはありません。 </p> <h2>Widgets</h2> <p> CustomElement で実装されたドメインデータを表示する UI 要素です。 </p> <p> ドメインデータの入出力を行い、他のアプリケーションに流用できないような造りになっています。 Protobuf で定義されたメッセージの入出力がメインとなるため、 属性ではなくプロパティやメソッドがメインの I/F となります。 </p> </Document> </DocsLayout>
-
-
-
@@ -1,46 +0,0 @@--- // 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"; --- <DocsLayout title="Globals"> <Document> <p> UA 間の差異を限りなく少なくするため、 Yamori の CSS は<strong> 全てのスタイルをリセット </strong> します。 リセットでは <strong> Yamori によって作成されていない全ての HTML 要素 </strong> に対して <code> all: unset; box-sizing: border-box </code> が与えられます。 </p> <Preview title="<button>BUTTON</button>"> <button type="button">BUTTON</button> </Preview> <p> これにより、全ての HTML 要素は CustomElement のホストと同様に <code> display: inline </code> が初期値となります。 基本的にインラインではない要素に対しては毎回 <code> display </code> プロパティを与える必要があります。 </p> <h2>Body Styles</h2> <p> スタイリングのベースラインとして <code><body></code> には継承される一部の CSS プロパティと背景色が与えられています。 </p> <ThemeMatrixPreview> <p>Paragraph – 段落</p> </ThemeMatrixPreview> </Document> </DocsLayout>
-
-
-
@@ -1,162 +0,0 @@--- // 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"; const gridItems = Array.from({ length: 20 }, (_, i) => { const name = `--space-px-${20 - i}`; const hasLabel = i <= 5; const properties = [ `width: var(${name})`, `height: var(${name})`, hasLabel ? "padding: var(--space-px-1) var(--space-px-2)" : null, ]; return { style: properties.filter((s) => typeof s === "string").join(";"), label: hasLabel ? name : null, }; }); --- <script> const interactiveContainer = document.getElementById("interactive_container"); const interactiveSlider = document.getElementById("interactive_slider"); const interactiveLabel = document.getElementById("interactive_label"); function syncInteractiveGap() { const size = interactiveSlider.value; interactiveContainer.style.gap = `var(--space-px-${size})`; interactiveLabel.textContent = `--space-x-${size}`; } document .getElementById("interactive_slider") .addEventListener("input", (event) => { syncInteractiveGap(); }); syncInteractiveGap(); </script> <style> .list.list { display: grid; grid-template-columns: var(--space-px-20); grid-template-rows: var(--space-px-20); width: 100%; padding: 0; } .box.box { display: inline-flex; align-items: end; border: 1px solid oklch(var(--color-border) / var(--alpha-border-strong)); grid-column: 1 / 2; grid-row: 1 / 2; font-size: var(--font-xs); font-family: var(--font-mono); border-radius: var(--space-px-1); color: oklch(var(--color-fg) / var(--alpha-fg-subtle)); list-style: none; } .box:not(:first-child) { border-inline-start-color: transparent; border-block-start-color: transparent; } .interactive-scroller { display: block; margin-block-end: var(--space-px-6); overflow-x: auto; } .interactive-container { display: flex; align-items: center; width: calc(var(--space-px-6) * 2 + var(--space-px-20)); } .fill { display: block; width: var(--space-px-6); height: var(--space-px-12); background-color: oklch(var(--color-fg) / var(--alpha-fg-strong)); border-radius: 2px; } .interactive-controls { display: flex; flex-direction: column; align-items: start; gap: var(--space-px-3); } .interactive-label { font-size: var(--font-sm); font-family: var(--font-mono); } .slider { border: 1px solid oklch(var(--color-border) / var(--alpha-border-subtle)); background-color: oklch(var(--color-fg) / 2%); border-radius: 0.6em; } .slider:focus-visible { border-color: oklch(var(--color-focus)); box-shadow: 0 0 0 var(--size-focus-ring) oklch(var(--color-focus) / var(--alpha-focus-ring)); } </style> <DocsLayout title="Spacings"> <Document> <p> 要素間・周囲の隙間の大きさは <code>--space-px-*</code> というカスタムプロパティによって一覧定義されています。 </p> <Preview title="List"> <ul class="list"> { gridItems.map(({ style, label }) => ( <li class="box" style={style}>{label}</li> )) } </ul> </Preview> <Preview title="Interactive Gap"> <div class="interactive-scroller"> <div id="interactive_container" class="interactive-container" style="gap: var(--space-px-1)" > <div class="fill" /> <div class="fill" /> </div> </div> <div class="interactive-controls"> <input id="interactive_slider" class="slider" type="range" value="1" min="1" max="20" step="1" aria-label="Gap size" /> <span id="interactive_label" class="interactive-label" >--space-px-1</span> </div> </Preview> </Document> </DocsLayout>
-
-
-
@@ -1,71 +0,0 @@--- // 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"; YamoriWorkspaceListEntry.register(); 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>
-
-
-
@@ -1,171 +0,0 @@--- // 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, RetryEvent, YamoriWorkspaceList, } from "../../../widgets/workspace-list/workspace-list.ts"; YamoriWorkspaceList.register(); 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=no_workspaces]")) { demo.setListResponse( create(ListResponseSchema, { result: { case: "ok", value: { workspaces: [], }, }, }), ); } for (const demo of document.querySelectorAll("[data-id=system_error]")) { demo.setListResponse( create(ListResponseSchema, { result: { case: "systemError", value: { code: "SAMPLE_ERR", message: "サンプルエラー", }, }, }), ); demo.addEventListener("retry", (event) => { if (event instanceof RetryEvent) { console.groupCollapsed("yamori-workspace-list.retry"); console.dir(event); console.groupEnd(); } }); } 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>No Workspaces</h2> <p> ワークスペースが一つも存在しない場合はフォールバックの UI を表示します。 <code>empty-fallback</code> スロットの内容をそのまま表示します。 </p> <Preview title="No workspaces"> <yamori-workspace-list data-id="no_workspaces"> <p slot="empty-fallback">FALLBACK</p> </yamori-workspace-list> </Preview> <h2>System Error</h2> <p> レスポンスが <code>system_error</code> の場合はエラー表示となります。 </p> <Preview title="result=system_error"> <yamori-workspace-list data-id="system_error"></yamori-workspace-list> </Preview> <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> <h2>Pending</h2> <p> <code>pending</code> 属性を指定することで読込中表示とすることができます。 </p> <Preview title="Pending"> <yamori-workspace-list pending data-id="demo"> <p slot="empty-fallback">FALLBACK</p> </yamori-workspace-list> </Preview> <Preview title="Pending / No Workspaces"> <yamori-workspace-list pending data-id="no_workspaces"> <p slot="empty-fallback">FALLBACK</p> </yamori-workspace-list> </Preview> <Preview title="Pending / System Error"> <yamori-workspace-list pending data-id="system_error" ></yamori-workspace-list> </Preview> </Document> </DocsLayout>
-
-
packages/gui/src/element.ts (deleted)
-
@@ -1,107 +0,0 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only export interface UIComponent< TagName extends string, Element extends typeof YamoriElement, > { readonly tagName: TagName; readonly constructor: Element; register(): boolean; } export interface UIComponentBuilder< TagName extends string, Element extends typeof YamoriElement, > { readonly tagName: TagName; readonly constructor: Element; readonly dependencies?: readonly UIComponent<string, typeof YamoriElement>[]; } /** * CustomElement を使いやすい I/F にラップする。 * 渡された CustomElement は自動的に `nonUAMixin` が適用され、 * `:is(:state(nonua), [x--_nonua])` でクエリできるようになる。 */ export function wrapElement< TagName extends string, Element extends typeof YamoriElement, >({ tagName, constructor, dependencies = [], }: UIComponentBuilder<TagName, Element>): UIComponent<TagName, Element> { return { tagName, constructor, register() { for (const dependency of dependencies) { dependency.register(); } if (customElements.get(tagName)) { return false; } customElements.define(tagName, constructor); return true; }, }; } /** * このプロジェクトで定義する全ての CustomElement の基底クラス。 * * CSS では未だにビルトインのタグとユーザ定義のタグをセレクトできず、また UA のスタイルを * 無効化する仕様も存在しない。そのため、UA が勝手に定義したスタイルを削除するためには、 * 1. Custom Element に識別用のマーカーを設定 * 2. マーカーのついていないものを全てリセット * とするしかない。この手法だと 3rd party の要素までもリセットされてしまうが、 * このプロジェクトにおいては 3rd party の Custom Element は利用しないためこの手法で * 問題はない。 * * 付与するマーカーは以下の通り: * * A) CustomStateSet の `nonua` ステート (Evergreen Browser) * B) `x--_nonua` 属性 * * 全てのマーカーに対応するには以下のようなセレクタが必要。 * * ```css * :is(:state(nonua), [x--_nonua]) * ``` */ export class YamoriElement extends HTMLElement { internals: ElementInternals; setCustomState(name: string): void { const internals = this.internals; try { internals.states.add(name); } catch { this.setAttribute(`x--_${name}`, ""); } } removeCustomState(name: string): void { const internals = this.internals; try { internals.states.delete(name); } catch { this.removeAttribute(`x--_${name}`); } } constructor() { super(); this.internals = this.attachInternals(); } connectedCallback() { this.setCustomState("nonua"); } }
-
-
packages/gui/src/global.css (deleted)
-
@@ -1,25 +0,0 @@/* @yamori/gui を利用するページで読み込む必要のあるカプセル化されていないスタイル。 * * SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> * 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)); } :not(:defined) { display: none; }
-
-
packages/gui/src/lib.ts (deleted)
-
@@ -1,34 +0,0 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { YamoriButton } from "./components/button/button.ts"; import { YamoriCallout } from "./components/callout/callout.ts"; import { YamoriEmpty } from "./components/empty/empty.ts"; import { YamoriWorkspaceList } from "./widgets/workspace-list/workspace-list.ts"; import { YamoriWorkspaceListEntry } from "./widgets/workspace-list-entry/workspace-list-entry.ts"; import { registerLucideIcons } from "./lucide/icons.ts"; export * from "./lucide/icons.ts"; export { YamoriButton, YamoriCallout, YamoriEmpty }; export { YamoriWorkspaceList, YamoriWorkspaceListEntry }; export function registerComponents(): void { YamoriButton.register(); YamoriCallout.register(); YamoriEmpty.register(); } export function registerWidgets(): void { YamoriWorkspaceList.register(); YamoriWorkspaceListEntry.register(); } export function register(): void { registerComponents(); registerWidgets(); registerLucideIcons(); }
-
-
packages/gui/src/lucide/builder.css (deleted)
-
@@ -1,13 +0,0 @@/* * SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> * SPDX-License-Identifier: AGPL-3.0-only */ :host { display: inline-flex; } .icon { width: auto; height: var(--lucide-icon-size, 1em); }
-
-
packages/gui/src/lucide/builder.ts (deleted)
-
@@ -1,33 +0,0 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { createElement, type IconNode } from "lucide"; import { wrapElement, YamoriElement } from "../element.ts"; import css from "./builder.css?inline"; export function createLucideElement(tagName: string, icon: IconNode) { return wrapElement({ tagName, constructor: class extends YamoriElement { constructor() { super(); const shadow = this.attachShadow({ mode: "open" }); const style = document.createElement("style"); style.textContent = css; shadow.appendChild(style); const body = createElement(icon); body.classList.add("icon"); shadow.appendChild(body); } override connectedCallback() { super.connectedCallback(); } }, }); }
-
-
packages/gui/src/lucide/icons.ts (deleted)
-
@@ -1,16 +0,0 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { BadgeInfo, BadgeX, FilePlus2 } from "lucide"; import { createLucideElement } from "./builder.ts"; export const LucideBadgeInfo = createLucideElement("lucide-badge-info", BadgeInfo); export const LucideBadgeX = createLucideElement("lucide-badge-x", BadgeX); export const LucideFilePlus2 = createLucideElement("lucide-file-plus2", FilePlus2); export function registerLucideIcons(): void { LucideBadgeInfo.register(); LucideBadgeX.register(); LucideFilePlus2.register(); }
-
-
packages/gui/src/reset.css (deleted)
-
@@ -1,12 +0,0 @@/* 究極のリセット CSS 。 * * SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> * SPDX-License-Identifier: AGPL-3.0-only */ /* UA スタイルを全部消す */ :where(body, body :not(script, style, svg, svg *, :is([x--_nonua], :state(nonua)))) { all: unset; box-sizing: border-box; }
-
-
packages/gui/src/vars/base.css (deleted)
-
@@ -1,71 +0,0 @@/* @yamori/gui で利用されるグローバルな CSS カスタムプロパティの全定義。 * * SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> * SPDX-License-Identifier: AGPL-3.0-only */ :root, :host { --font-sans: "Inter Variable", "IBM Plex Sans JP", sans-serif; --font-mono: "Monaspace Neon", monospace; /* Plastic Ratio の近似値 */ --scale: calc(53 / 40); --hue: 30deg; --chroma: 0.5%; --color-bg-l: 99%; --color-bg: var(--color-bg-l) var(--chroma) var(--hue); --color-fg-l: 25%; --color-fg: var(--color-fg-l) var(--chroma) var(--hue); --alpha-fg-strong: 95%; --alpha-fg-medium: 85%; --alpha-fg-subtle: 60%; --color-border-l: 20%; --color-border: var(--color-border-l) var(--chroma) var(--hue); --alpha-border-strong: 50%; --alpha-border-medium: 25%; --alpha-border-subtle: 10%; --color-focus-l: 60%; --color-focus-c: 40%; --color-focus-h: 260deg; --color-focus: var(--color-focus-l) var(--color-focus-c) var(--color-focus-h); --alpha-focus-ring: 30%; /* pow() が広く実装されて1年程度しか経っていないため我慢 */ --space-px-1: 2px; --space-px-2: calc(var(--space-px-1) * var(--scale)); --space-px-3: calc(var(--space-px-2) * var(--scale)); --space-px-4: calc(var(--space-px-3) * var(--scale)); --space-px-5: calc(var(--space-px-4) * var(--scale)); --space-px-6: calc(var(--space-px-5) * var(--scale)); --space-px-7: calc(var(--space-px-6) * var(--scale)); --space-px-8: calc(var(--space-px-7) * var(--scale)); --space-px-9: calc(var(--space-px-8) * var(--scale)); --space-px-10: calc(var(--space-px-9) * var(--scale)); --space-px-11: calc(var(--space-px-10) * var(--scale)); --space-px-12: calc(var(--space-px-11) * var(--scale)); --space-px-13: calc(var(--space-px-12) * var(--scale)); --space-px-14: calc(var(--space-px-13) * var(--scale)); --space-px-15: calc(var(--space-px-14) * var(--scale)); --space-px-16: calc(var(--space-px-15) * var(--scale)); --space-px-17: calc(var(--space-px-16) * var(--scale)); --space-px-18: calc(var(--space-px-17) * var(--scale)); --space-px-19: calc(var(--space-px-18) * var(--scale)); --space-px-20: calc(var(--space-px-19) * var(--scale)); --size-focus-ring: var(--space-px-4); --font-md: 1rem; --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; }
-
-
packages/gui/src/vars/contrast.css (deleted)
-
@@ -1,22 +0,0 @@/* コントラスト強調時に上書きするカスタムプロパティ。 * base.css の後に読み込まれる必要がある。 * * SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> * SPDX-License-Identifier: AGPL-3.0-only */ :root, :host { --color-fg-l: 1%; --alpha-fg-strong: 100%; --alpha-fg-medium: 95%; --alpha-fg-subtle: 80%; --color-border-l: 3%; --alpha-border-strong: 90%; --alpha-border-medium: 50%; --alpha-border-subtle: 30%; --alpha-focus-ring: 100%; --size-focus-ring: var(--space-px-1); }
-
-
packages/gui/src/vars/dark-contrast.css (deleted)
-
@@ -1,13 +0,0 @@/* ダークモード有効且つコントラスト強調時に上書きするカスタムプロパティ。 * base.css の後に読み込まれる必要がある。 * * SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> * SPDX-License-Identifier: AGPL-3.0-only */ :root, :host { --color-bg-l: 10%; --color-fg-l: 99%; --color-border-l: 95%; }
-
-
packages/gui/src/vars/dark.css (deleted)
-
@@ -1,19 +0,0 @@/* ダークモード有効時に上書きするカスタムプロパティ。 * base.css の後に読み込まれる必要がある。 * * SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> * SPDX-License-Identifier: AGPL-3.0-only */ :root, :host { --color-bg-l: 15%; --color-fg-l: 95%; --alpha-fg-medium: 92%; --color-border-l: 90%; --color-focus-l: 70%; --alpha-focus-ring: 70%; }
-
-
-
@@ -1,45 +0,0 @@/* * SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> * SPDX-License-Identifier: AGPL-3.0-only */ :not(:is(style, script, :state(nonua), [x--_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), [x--_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; }
-
-
-
@@ -1,106 +0,0 @@// 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({ tagName: "yamori-workspace-list-entry", dependencies: [YamoriButton], constructor: 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"; } override connectedCallback(): void { super.connectedCallback(); 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)); }); }; get workspace(): Workspace | null { return this.#workspace; } set workspace(workspace: Workspace) { this.setWorkspace(workspace); } setWorkspace(workspace: Workspace) { this.#workspace = workspace; this.#render(); } }, });
-
-
-
@@ -1,42 +0,0 @@/* * SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> * SPDX-License-Identifier: AGPL-3.0-only */ :not(:is(style, script, :state(nonua), [x--_nonua])) { all: unset; box-sizing: border-box; } :host { position: relative; display: block; } .list { display: flex; flex-direction: column; } .list > :not(:last-child) { border-bottom: 1px solid oklch(var(--color-border) / var(--alpha-border-subtle)); } .fallback { display: contents; } .revalidating { display: block; position: absolute; top: 0; right: 0; padding: var(--space-px-3) var(--space-px-5); font-size: var(--font-sm); border: 1px solid oklch(var(--color-border) / var(--alpha-border-subtle)); background-color: oklch(var(--color-bg)); border-radius: var(--space-px-2); box-shadow: 2px 2px 4px 0 oklch(0% 0% 0deg / 5%); color: oklch(var(--color-fg) / var(--alpha-fg-medium)); }
-
-
-
@@ -1,194 +0,0 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { fromJsonString } from "@bufbuild/protobuf"; import { html, nothing, render, type TemplateResult } from "lit-html"; import { repeat } from "lit-html/directives/repeat.js"; import { type ListResponse, ListResponseSchema, } from "@yamori/proto/yamori/workspace/v1/list_response_pb.js"; import { wrapElement, YamoriElement } from "../../element.ts"; import { YamoriButton } from "../../components/button/button.ts"; import { YamoriCallout } from "../../components/callout/callout.ts"; import { YamoriEmpty } from "../../components/empty/empty.ts"; import { LucideBadgeX } from "../../lucide/icons.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"; export class RetryEvent extends Event { constructor() { super("retry", { cancelable: true, }); } } type ObservedAttributes = readonly ["pending", "list-response"]; export const YamoriWorkspaceList = wrapElement({ tagName: "yamori-workspace-list", dependencies: [ YamoriButton, YamoriCallout, YamoriEmpty, LucideBadgeX, YamoriWorkspaceListEntry, ], constructor: class extends YamoriElement { static get observedAttributes(): ObservedAttributes { return ["pending", "list-response"]; } #pending: boolean = this.hasAttribute("pending"); #data: ListResponse | null = this.#parseListResponseAttribute(); #shadow: ShadowRoot; 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; } } override connectedCallback(): void { super.connectedCallback(); this.#render(); } constructor() { super(); this.#shadow = this.attachShadow({ mode: "open", }); } setListResponse(listResponse: ListResponse): void { this.#data = listResponse; this.#render(); } #pendingIndicator(): TemplateResult { if (!this.#pending) { return html`${nothing}`; } return html` <span class="revalidating">データ取得中...</span> `; } #view(): TemplateResult { // データがない場合はどうしようもないので読込中としてお茶を濁す if (!this.#data) { return html` ${this.#pendingIndicator()} <p>取得中...</p> `; } switch (this.#data.result.case) { case "systemError": { const onRetry = (event: MouseEvent) => { event.preventDefault(); event.stopPropagation(); this.dispatchEvent(new RetryEvent()); }; return html` ${this.#pendingIndicator()} <yamori-empty> <lucide-badge-x slot="icon"></lucide-badge-x> <span slot="title">取得エラー</span> ワークスペース一覧の取得に失敗しました。<br/> エラーコード: ${this.#data.result.value.code} <yamori-button slot="action" ?pending=${this.#pending} @click=${onRetry}> 再取得 </yamori-button> </yamori-empty> `; } case "ok": { const { workspaces } = this.#data.result.value; if (workspaces.length === 0) { return html` ${this.#pendingIndicator()} <slot name="empty-fallback" class="fallback"></slot> `; } return html` ${this.#pendingIndicator()} <ul class="list"> ${repeat( workspaces, (workspace) => workspace.id?.value, (workspace) => html` <yamori-workspace-list-entry .workspace=${workspace}></yamori-workspace-list-entry> `, )} </ul> `; } default: { return html`<p>データが破損しています</p>`; } } } #render(): void { render( html` <style>${css}</style> ${this.#view()} `, this.#shadow, ); } }, });
-
-
-
@@ -10,6 +10,5 @@ "declaration": true,"emitDeclarationOnly": true, "outDir": "./types" }, "include": ["src/**/*.ts"], "exclude": ["src/docs/*.ts"] "include": ["src/**/*.ts", "src/**/*.tsx"] }
-
-
packages/gui/tsconfig.json (deleted)
-
@@ -1,10 +0,0 @@{ "extends": "../../tsconfig.jsonc", "compilerOptions": { "moduleResolution": "Bundler", "noEmit": true, "lib": ["ES2020", "DOM", "DOM.iterable"], "types": ["vite/client"] }, "include": ["*.ts", "src/**/*.ts"] }
-
-
-
packages/gui/vite.config.ts (deleted)
-
@@ -1,67 +0,0 @@// Web 向けのバンドラー、 Vite の設定ファイル。 // <https://vite.dev/> // // SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { relative } from "node:path"; import { defineConfig, type UserConfig, type Rollup } from "vite"; const srcDir = new URL("./src/", import.meta.url); function libraryCSSFiles( files: readonly URL[], ): Extract<Rollup.InputOption, Record<string, string>> { const entries: ReturnType<typeof libraryCSSFiles> = {}; for (const file of files) { const displayPath = relative(process.cwd(), file.pathname); if (!file.toString().startsWith(srcDir.toString())) { console.warn( `You cannot specify CSS file outside src/ directory as library output: ${displayPath}`, ); continue; } if (!file.pathname.endsWith(".css")) { console.warn( `You cannot pass non-CSS file to libraryCSSFiles function in Vite config: ${displayPath}`, ); continue; } const name = file.toString().slice(srcDir.toString().length, -4); entries[name] = file.pathname; } return entries; } export default defineConfig(async () => { return { root: srcDir.pathname, build: { emptyOutDir: true, outDir: "../dist", cssCodeSplit: true, lib: { entry: { ...libraryCSSFiles([ new URL("./src/all.css", import.meta.url), new URL("./src/reset.css", import.meta.url), new URL("./src/global.css", import.meta.url), new URL("./src/vars/base.css", import.meta.url), new URL("./src/vars/dark.css", import.meta.url), new URL("./src/vars/contrast.css", import.meta.url), new URL("./src/vars/dark-contrast.css", import.meta.url), ]), lib: new URL("./lib.ts", srcDir).pathname, }, formats: ["es"], }, }, } satisfies UserConfig; });
-
-
-
@@ -3,10 +3,6 @@ ## SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> # SPDX-License-Identifier: AGPL-3.0-only # What: ビルドされた PWA のページとアセット。 # What: ビルドされたページとアセット。 # Why: 編集するものではないため。 /dist # What: Gleam のビルドファイルや中間ファイルのディレクトリ。 # Why: ソースコードを基に毎回生成されるため。編集はしない。 /build
-
-
packages/pwa/gleam.toml (deleted)
-
@@ -1,14 +0,0 @@# Gleam のプロジェクトマニフェスト。 # # SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> # SPDX-License-Identifier: AGPL-3.0-only name = "yamori_pwa" version = "0.1.0" # Web フロントエンド用途のみのため、フラグを指定しなくてすむように固定する。 target = "javascript" [dependencies] gleam_stdlib = ">= 0.45.0 and < 1.0.0" gleam_json = ">= 2.1.0 and < 3.0.0"
-
-
packages/pwa/manifest.toml (deleted)
-
@@ -1,11 +0,0 @@# This file was generated by Gleam # You typically do not need to edit this file packages = [ { name = "gleam_json", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "0A57FB5666E695FD2BEE74C0428A98B0FC11A395D2C7B4CDF5E22C5DD32C74C6" }, { name = "gleam_stdlib", version = "0.45.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "206FCE1A76974AECFC55AEBCD0217D59EDE4E408C016E2CFCCC8FF51278F186E" }, ] [requirements] gleam_json = { version = ">= 2.1.0 and < 3.0.0" } gleam_stdlib = { version = ">= 0.45.0 and < 1.0.0" }
-
-
packages/pwa/manifest.toml.license (deleted)
-
@@ -1,5 +0,0 @@Gleam のロックファイル。 コンパイラが常に全書き換えする "仕様" のためヘッダを書いても消される。 SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> SPDX-License-Identifier: AGPL-3.0-only
-
-
-
@@ -3,23 +3,24 @@ "name": "@yamori/pwa","private": true, "type": "module", "scripts": { "check:ts": "tsc", "check:gleam": "gleam check", "check": "bun check:ts && bun check:gleam", "check": "tsc", "dev": "vite", "build": "vite build", "clean": "rm -rf dist" }, "dependencies": { "@bufbuild/protobuf": "^2.2.2", "@yamori/gui": "workspace:*", "@yamori/proto": "workspace:*", "idb": "^8.0.0" "@yamori/react_ui": "workspace:*", "idb": "^8.0.0", "react": "^19.0.0", "react-dom": "^19.0.0" }, "devDependencies": { "@pocka/rollup-plugin-gleam": "^0.1.2", "@types/bun": "^1.1.14", "fake-indexeddb": "^6.0.0", "@types/react": "^19.0.2", "@types/react-dom": "^19.0.2", "@vitejs/plugin-react": "^4.3.4", "typescript": "^5.7.2", "vite": "^6.0.2" }
-
-
-
@@ -1,4 +1,4 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only export function main(container: Element): void; /// <reference types="vite/client" />
-
-
packages/pwa/src/app.gleam (deleted)
-
@@ -1,88 +0,0 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import backend import dom import gleam/json import gleam/string import protobuf fn encode_list_request() -> protobuf.Binary { json.object([]) |> protobuf.encode(protobuf.list_request()) } fn decode_list_response( binary: protobuf.Binary, ) -> protobuf.Message(protobuf.ListResponse) { binary |> protobuf.decode(protobuf.list_response()) } pub fn main(container: dom.Node(dom.Element)) -> Nil { let loading = dom.html("p") |> dom.append_child(dom.text("Loading...")) container |> dom.append_child(loading) use worker_result <- backend.load_worker() dom.detach_node(loading) case worker_result { Ok(worker) -> { let button = dom.html("yamori-button") |> dom.append_child(dom.text("List Workspaces")) let pre = dom.html("pre") let output = dom.html("output") container |> dom.append_child(button) |> dom.append_child(dom.append_child(pre, output)) { use _event <- dom.add_event_listener(button, "click") button |> dom.set_attribute("pending", "") use response <- backend.request( worker, "yamori.workspace.v1.KeyValueStorageBasedWorkspaceService", "List", encode_list_request(), ) button |> dom.remove_attribute("pending") let text = decode_list_response(response) |> protobuf.to_json |> string.append("\n") output |> dom.append_child(dom.text(text)) Nil } } Error(error) -> { let feedback = dom.html("p") |> dom.append_child(dom.text("Failed to load worker: ")) |> dom.append_child( dom.text(case error { backend.DecodeError(_errors) -> "Detected broken inner system message." backend.WorkerInitializeError(text) -> text }), ) container |> dom.append_child(feedback) Nil } } }
-
-
packages/pwa/src/backend.ffi.ts (deleted)
-
@@ -1,97 +0,0 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { isValidMessage, type Message } from "@/worker/message.ts"; type BackendResult<T, E> = | { ok: true; data: T; } | { ok: false; error: E; }; export function loadWorker( onLoaded: (worker: BackendResult<Worker, string>) => void, ): void { const worker = new Worker(new URL("./worker/main.ts", import.meta.url), { type: "module", }); const eventListener = (event: ErrorEvent | MessageEvent) => { worker.removeEventListener("error", eventListener); worker.removeEventListener("message", eventListener); if (event instanceof ErrorEvent) { onLoaded({ ok: false, error: String(event.error), }); return; } if (event.data !== "ready") { onLoaded({ ok: false, error: `Unexpected initial message received: ${event.data}`, }); return; } onLoaded({ ok: true, data: worker, }); }; worker.addEventListener("error", eventListener); worker.addEventListener("message", eventListener); } export function request( worker: Worker, service: string, method: string, request: Uint8Array, onResponse: (response: Uint8Array) => void, ): void { const requestID = crypto.randomUUID(); const handler = (event: MessageEvent) => { if (!isValidMessage(event.data) || event.data.id !== requestID) { return; } worker.removeEventListener("message", handler); if (import.meta.env.NODE_ENV === "development" && event.data.service !== service) { console.warn( `Worker backend returned mismatching service: sent=${service}, received=${event.data.service}`, ); } if (import.meta.env.NODE_ENV === "development" && event.data.method !== method) { console.warn( `Worker backend returned mismatching service: sent=${method}, received=${event.data.method}`, ); } onResponse(event.data.data); }; worker.addEventListener("message", handler); worker.postMessage( { id: requestID, service, method, data: request, } satisfies Message, { transfer: [request.buffer], }, ); }
-
-
packages/pwa/src/backend.gleam (deleted)
-
@@ -1,74 +0,0 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import gleam/dynamic import gleam/result import protobuf fn decode_backend_result( input: dynamic.Dynamic, data: dynamic.Decoder(a), error: dynamic.Decoder(b), ) -> Result(Result(a, b), dynamic.DecodeErrors) { input |> dynamic.decode2( fn(ok, obj) { case ok { True -> obj |> dynamic.decode1(Ok, dynamic.field("data", data)) False -> obj |> dynamic.decode1(Error, dynamic.field("error", error)) } }, dynamic.field("ok", dynamic.bool), dynamic.dynamic, ) |> result.flatten } @external(javascript, "@/backend.ffi.ts", "loadWorker") fn load_worker_internal(on_loaded: fn(dynamic.Dynamic) -> Nil) -> Nil pub opaque type Worker { Worker(ref: dynamic.Dynamic) } pub type WorkerLoadError { DecodeError(dynamic.DecodeErrors) WorkerInitializeError(String) } pub fn load_worker(on_loaded: fn(Result(Worker, WorkerLoadError)) -> Nil) -> Nil { use backend_result <- load_worker_internal() backend_result |> decode_backend_result( dynamic.decode1(Worker, dynamic.dynamic), dynamic.decode1(WorkerInitializeError, dynamic.string), ) |> result.map_error(DecodeError) |> result.flatten |> on_loaded } @external(javascript, "@/backend.ffi.ts", "request") fn request_internal( worker: dynamic.Dynamic, service: String, method: String, request: protobuf.Binary, on_response: fn(protobuf.Binary) -> Nil, ) -> Nil pub fn request( worker: Worker, service: String, method: String, request: protobuf.Binary, on_response: fn(protobuf.Binary) -> Nil, ) -> Nil { request_internal(worker.ref, service, method, request, on_response) }
-
-
packages/pwa/src/components.ts (deleted)
-
@@ -1,8 +0,0 @@// 起動時にドキュメントの読み込みを待たずに実行されるスクリプト。 // // SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { register } from "@yamori/gui"; register();
-
-
packages/pwa/src/dom.ffi.ts (deleted)
-
@@ -1,42 +0,0 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only export function createTextNode(text: string): Text { return document.createTextNode(text); } export function createHTMLElement(tagName: string): HTMLElement { return document.createElement(tagName); } export function setAttribute<T extends Element>( element: T, name: string, value: string, ): T { element.setAttribute(name, value); return element; } export function removeAttribute<T extends Element>(element: T, name: string): T { element.removeAttribute(name); return element; } export function appendChild<T extends Element>(parent: T, node: Node): T { parent.appendChild(node); return parent; } export function removeNode<T extends Node>(node: T): void { node.parentNode?.removeChild(node); } export function addEventListener<T extends Element>( element: T, eventName: string, listener: (event: Event) => void, ): T { element.addEventListener(eventName, listener); return element; }
-
-
packages/pwa/src/dom.gleam (deleted)
-
@@ -1,39 +0,0 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import gleam/dynamic.{type Dynamic} pub type TextNode pub type Element pub type Node(a) @external(javascript, "@/dom.ffi.ts", "createHTMLElement") pub fn html(tag_name: String) -> Node(Element) @external(javascript, "@/dom.ffi.ts", "createTextNode") pub fn text(content: String) -> Node(TextNode) @external(javascript, "@/dom.ffi.ts", "setAttribute") pub fn set_attribute( element: Node(Element), name: String, value: String, ) -> Node(Element) @external(javascript, "@/dom.ffi.ts", "removeAttribute") pub fn remove_attribute(element: Node(Element), name: String) -> Node(Element) @external(javascript, "@/dom.ffi.ts", "appendChild") pub fn append_child(parent: Node(Element), child: Node(a)) -> Node(Element) @external(javascript, "@/dom.ffi.ts", "removeNode") pub fn detach_node(node: Node(a)) -> Nil @external(javascript, "@/dom.ffi.ts", "addEventListener") pub fn add_event_listener( element: Node(Element), event_name: String, callback: fn(Dynamic) -> Nil, ) -> Nil
-
-
-
@@ -5,11 +5,11 @@ <meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /> <meta http-equiv="content-security-policy" content="default-src 'none'; img-src 'self' data:; script-src 'self'; connect-src 'self'; style-src 'self' 'unsafe-inline'; worker-src 'self'; font-src 'self' data:" content="default-src 'none'; img-src 'self' data:; script-src 'self'; connect-src 'self'; style-src 'self' 'unsafe-inline'; worker-src 'self'; font-src 'self'" /> <script type="module" async src="./components.ts"></script> <script type="module" src="./main.ts"></script> <script type="module" src="./main.tsx"></script> <link rel="stylesheet" href="./styles.css" /> <title>Yamori</title> </head> <body></body> </html>
-
-
-
@@ -0,0 +1,13 @@/* * SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> * SPDX-License-Identifier: AGPL-3.0-only */ .theme { position: absolute; inset: 0; box-sizing: border-box; padding: var(--space-3); overflow: auto; }
-
-
packages/pwa/src/main.ts (deleted)
-
@@ -1,6 +0,0 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { main } from "./app.gleam"; main(document.body);
-
-
-
@@ -0,0 +1,91 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { ListRequestSchema } from "@yamori/proto/yamori/workspace/v1/list_request_pb.js"; import { ListResponseSchema } from "@yamori/proto/yamori/workspace/v1/list_response_pb.js"; import { ProtoRPCProvider, type ProtoRPC, useMethodQuery, ThemeProvider, WorkspaceList, } from "@yamori/react_ui"; import { type FC } from "react"; import { createRoot } from "react-dom/client"; import css from "./main.module.css"; import { Message, isValidMessage } from "./worker/message.ts"; const worker = new Worker(new URL("./worker/main.ts", import.meta.url), { type: "module", }); const App: FC = () => { const list = useMethodQuery({ service: "yamori.workspace.v1.KeyValueStorageBasedWorkspaceService", method: "List", request: { schema: ListRequestSchema, data: {}, }, response: { schema: ListResponseSchema, }, }); if (!list.isSuccess) { return <p>Loading</p>; } if (list.data.result.case === "systemError") { return <p>System Error: {list.data.result.value.code}</p>; } if (!list.data.result.case) { return <p>Message Error (MISSING_CASE)</p>; } return <WorkspaceList workspaces={list.data.result.value.workspaces} />; }; const root = createRoot(document.body); worker.addEventListener("message", (event) => { if (event.data !== "ready") { return; } const rpc: ProtoRPC = { send(service, method, data) { const id = crypto.randomUUID(); return new Promise((resolve) => { const onMessage = (event: MessageEvent) => { if (!isValidMessage(event.data) || event.data.id !== id) { return; } resolve(event.data.data); worker.removeEventListener("message", onMessage); }; worker.addEventListener("message", onMessage); worker.postMessage({ id, service, method, data, } satisfies Message); }); }, }; root.render( <ProtoRPCProvider rpc={rpc}> <ThemeProvider className={css.theme}> <App /> </ThemeProvider> </ProtoRPCProvider>, ); });
-
-
packages/pwa/src/protobuf.ffi.ts (deleted)
-
@@ -1,40 +0,0 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { create, fromBinary, toBinary, type DescMessage, type MessageInitShape, type MessageShape, } from "@bufbuild/protobuf"; // TODO: Move these out to services/ or similar. import { ListRequestSchema } from "@yamori/proto/yamori/workspace/v1/list_request_pb.js"; import { ListResponseSchema } from "@yamori/proto/yamori/workspace/v1/list_response_pb.js"; export function ListRequest() { return ListRequestSchema; } export function ListResponse() { return ListResponseSchema; } export function decode<Message extends DescMessage>(binary: Uint8Array, schema: Message) { return fromBinary(schema, binary); } export function encode<Message extends DescMessage>( payload: MessageInitShape<Message>, schema: Message, ) { return toBinary(schema, create(schema, payload)); } export function toJSON<Message extends DescMessage>( message: MessageShape<Message>, ): string { return JSON.stringify(message, null, 2); }
-
-
packages/pwa/src/protobuf.gleam (deleted)
-
@@ -1,29 +0,0 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import gleam/json pub type Message(a) pub type Schema(a) pub type Binary @external(javascript, "@/protobuf.ffi.ts", "decode") pub fn decode(binary: Binary, schema: Schema(a)) -> Message(a) @external(javascript, "@/protobuf.ffi.ts", "encode") pub fn encode(payload: json.Json, schema: Schema(a)) -> Binary pub type ListRequest @external(javascript, "@/protobuf.ffi.ts", "ListRequest") pub fn list_request() -> Schema(ListRequest) pub type ListResponse @external(javascript, "@/protobuf.ffi.ts", "ListResponse") pub fn list_response() -> Schema(ListResponse) @external(javascript, "@/protobuf.ffi.ts", "toJSON") pub fn to_json(message: Message(a)) -> String
-
-
-
@@ -3,4 +3,4 @@ * SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com>* SPDX-License-Identifier: AGPL-3.0-only */ @import "@yamori/gui/all.css"; @import "@yamori/react_ui/styles.css";
-
-
-
@@ -3,10 +3,8 @@ "extends": "../../tsconfig.jsonc","compilerOptions": { "moduleResolution": "Bundler", "noEmit": true, "lib": ["ES2020", "DOM"], "paths": { "@/*": ["./src/*"] } "lib": ["ES2020", "DOM", "DOM.iterable"], "jsx": "react-jsx" }, "include": ["*.ts", "src/**/*.ts"] "include": ["*.ts", "src/**/*.ts", "src/**/*.tsx"] }
-
-
-
@@ -4,7 +4,7 @@ //// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { gleam } from "@pocka/rollup-plugin-gleam"; import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; export default defineConfig({
-
@@ -13,14 +13,5 @@ build: {emptyOutDir: true, outDir: "../dist", }, resolve: { alias: { "@": new URL("./src", import.meta.url).pathname, }, }, plugins: [ gleam({ gleamToml: new URL("./gleam.toml", import.meta.url), }), ], plugins: [react()], });
-
-
-
@@ -0,0 +1,18 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { type StorybookConfig } from "@storybook/react-vite"; export default { stories: ["../src/**/*.stories.tsx"], addons: ["@storybook/addon-essentials", "@storybook/addon-interactions"], framework: { name: "@storybook/react-vite", options: {}, }, core: { disableTelemetry: true, disableWhatsNewNotifications: true, enableCrashReports: false, }, } satisfies StorybookConfig;
-
-
-
@@ -0,0 +1,16 @@/* * SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> * SPDX-License-Identifier: AGPL-3.0-only */ .theme { position: absolute; inset: 0; box-sizing: border-box; overflow: auto; } :global(.sb-main-padded) .theme { padding: var(--space-3); }
-
-
-
@@ -0,0 +1,17 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { Preview } from "@storybook/react"; import { ThemeProvider } from "../src/lib.ts"; import css from "./preview.module.css"; export default { decorators: [ (Story) => ( <ThemeProvider className={css.theme}> <Story /> </ThemeProvider> ), ], } satisfies Preview;
-
-
-
@@ -0,0 +1,42 @@{ "name": "@yamori/react_ui", "private": true, "type": "module", "scripts": { "dev": "storybook dev", "build": "vite build && tsc -p tsconfig.build.jsonc", "clean": "rm -rf dist", "check": "tsc" }, "exports": { ".": { "types": "./types/lib.d.ts", "default": "./dist/lib.js" }, "./styles.css": { "default": "./dist/styles.css" } }, "peerDependencies": { "react": "19.x.x" }, "dependencies": { "@bufbuild/protobuf": "^2.2.2", "@radix-ui/themes": "^3.1.6", "@tanstack/react-query": "^5.62.8", "@yamori/proto": "workspace:*" }, "devDependencies": { "@storybook/addon-essentials": "^8.4.7", "@storybook/addon-interactions": "^8.4.7", "@storybook/react": "^8.4.7", "@storybook/react-vite": "^8.4.7", "@storybook/test": "^8.4.7", "@types/react": "^19.0.2", "@vitejs/plugin-react": "^4.3.4", "react": "19.x.x", "react-dom": "^19.0.0", "typescript": "^5.7.2", "vite": "^6.0.2" } }
-
-
-
@@ -0,0 +1,35 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { Theme } from "@radix-ui/themes"; import { type FC, type ReactNode, useEffect, useState } from "react"; export interface ThemeProviderProps { className?: string | undefined; children: ReactNode; } export const ThemeProvider: FC<ThemeProviderProps> = ({ children, className }) => { const [isDark] = useState(() => window.matchMedia("(prefers-color-scheme: dark)")); // `MediaQueryList.matches` は常に最新の状態を表すため、別に保存してその値を読む // 必要はない。この state は単に再レンダリングを引き起こすためだけのもの。 const [, setUpdater] = useState(() => isDark.matches); useEffect(() => { const listener = (event: MediaQueryListEvent) => { setUpdater(event.matches); }; isDark.addEventListener("change", listener); return () => void isDark.removeEventListener("change", listener); }, [isDark]); return ( <Theme className={className} appearance={isDark.matches ? "dark" : "light"}> {children} </Theme> ); };
-
-
-
@@ -0,0 +1,32 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { create } from "@bufbuild/protobuf"; import type { Meta, StoryObj } from "@storybook/react"; import { WorkspaceSchema } from "@yamori/proto/yamori/workspace/v1/workspace_pb.js"; import { WorkspaceList } from "./WorkspaceList.tsx"; export default { component: WorkspaceList, args: { workspaces: [ create(WorkspaceSchema, { id: { value: "ws-foo", }, displayName: "Foo", }), create(WorkspaceSchema, { id: { value: "ws-bar", }, displayName: "Bar", }), ], }, } satisfies Meta<typeof WorkspaceList>; type Story = StoryObj<typeof WorkspaceList>; export const Default: Story = {};
-
-
-
@@ -0,0 +1,29 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { Box, Button, Card, Flex, Text } from "@radix-ui/themes"; import { type Workspace } from "@yamori/proto/yamori/workspace/v1/workspace_pb.js"; import { type FC } from "react"; export interface WorkspaceListProps { workspaces: readonly Workspace[]; } export const WorkspaceList: FC<WorkspaceListProps> = ({ workspaces }) => { return ( <Flex role="list" direction="column" gap="3"> {workspaces.map((workspace) => ( <Card key={workspace.id?.value} role="listitem"> <Flex direction="column" gap="2"> <Box> <Text weight="bold">{workspace.displayName}</Text> </Box> <Flex justify="end"> <Button variant="soft">開く</Button> </Flex> </Flex> </Card> ))} </Flex> ); };
-
-
-
@@ -0,0 +1,153 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { type DescMessage, type MessageInitShape, type MessageShape, create, toBinary, fromBinary, } from "@bufbuild/protobuf"; import { QueryClient, QueryClientProvider, type MutationOptions, type UseMutationResult, useMutation, type QueryOptions, type UseQueryResult, useQuery, } from "@tanstack/react-query"; import { createContext, type FC, type ReactNode, use, useState } from "react"; export interface ProtoRPC { send(service: string, method: string, data: Uint8Array): Promise<Uint8Array>; } const Context = createContext<ProtoRPC | null>(null); export interface ProtoRPCProviderProps { children: ReactNode; rpc: ProtoRPC; } export const ProtoRPCProvider: FC<ProtoRPCProviderProps> = ({ children, rpc }) => { const [queryClient] = useState(() => new QueryClient()); return ( <Context.Provider value={rpc}> <QueryClientProvider client={queryClient}>{children}</QueryClientProvider> </Context.Provider> ); }; export function useRPC(): ProtoRPC { const rpc = use(Context); if (!rpc) { throw new Error("`useService` MUST be called inside `ServiceProvider`"); } return rpc; } export interface UseMethodQueryParams< Request extends DescMessage, Response extends DescMessage, > { readonly service: string; readonly method: string; readonly request: { readonly schema: Request; readonly data: MessageInitShape<Request>; }; readonly response: { readonly schema: Response; }; readonly options?: Omit<QueryOptions<MessageShape<Response>>, "queryKey" | "queryFn">; } export function useMethodQuery< Request extends DescMessage, Response extends DescMessage, >({ service, method, request, response, options, }: UseMethodQueryParams<Request, Response>): UseQueryResult< MessageShape<Response>, unknown > { const rpc = useRPC(); return useQuery({ ...options, queryKey: [service, method, request.data], async queryFn() { const received = await rpc.send( service, method, toBinary(request.schema, create(request.schema, request.data)), ); return fromBinary(response.schema, received); }, }); } export interface UseMethodMutationParams< Request extends DescMessage, Response extends DescMessage, > { readonly service: string; readonly method: string; readonly request: { readonly schema: Request; }; readonly response: { readonly schema: Response; }; readonly options?: Omit< MutationOptions<MessageShape<Response>, unknown, MessageInitShape<Request>>, "mutationFn" >; } export function useMethodMutation< Request extends DescMessage, Response extends DescMessage, >({ service, method, request, response, options, }: UseMethodMutationParams<Request, Response>): UseMutationResult< MessageShape<Response>, unknown, MessageInitShape<Request> > { const rpc = useRPC(); return useMutation({ ...options, async mutationFn(req) { const received = await rpc.send( service, method, toBinary(request.schema, create(request.schema, req)), ); return fromBinary(response.schema, received); }, }); }
-
-
-
@@ -0,0 +1,9 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import "@radix-ui/themes/styles.css"; export * from "./components/ThemeProvider.tsx"; export * from "./components/WorkspaceList.tsx"; export * from "./contexts/Service.tsx";
-
-
-
@@ -0,0 +1,17 @@{ "extends": "../../tsconfig.jsonc", "compilerOptions": { "moduleResolution": "Bundler", "noEmit": true, "lib": ["ES2020", "DOM", "DOM.iterable"], "types": ["vite/client"], "jsx": "react-jsx" }, "include": [ "*.ts", ".storybook/*.ts", ".storybook/*.tsx", "src/**/*.ts", "src/**/*.tsx" ] }
-
-
-
@@ -0,0 +1,23 @@// Web 向けのバンドラー、 Vite の設定ファイル。 // <https://vite.dev/> // // SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; export default defineConfig({ build: { lib: { entry: "src/lib.ts", formats: ["es"], fileName: "lib", cssFileName: "styles", }, rollupOptions: { external: ["react", /^react\//, "@radix-ui/themes"], }, }, plugins: [react()], });
-