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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
587 changes: 239 additions & 348 deletions bun.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions deno.lock

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

5 changes: 4 additions & 1 deletion js/hang-demo/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import "./highlight";
import "@moq/hang-ui/watch/element";
import { setBasePath } from "@moq/hang-ui/utilities";

setBasePath(__HANG_UI_ASSETS_PATH__);

import "@moq/hang-ui/watch/element";
import HangSupport from "@moq/hang/support/element";
import HangWatch from "@moq/hang/watch/element";

Expand Down
5 changes: 4 additions & 1 deletion js/hang-demo/src/meet.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import "./highlight";
import "@moq/hang-ui/publish/element";
import { setBasePath } from "@moq/hang-ui/utilities";

setBasePath(__HANG_UI_ASSETS_PATH__);

import "@moq/hang-ui/publish/element";
import HangMeet from "@moq/hang/meet/element";
import HangPublish from "@moq/hang/publish/element";
import HangSupport from "@moq/hang/support/element";
Expand Down
4 changes: 4 additions & 0 deletions js/hang-demo/src/publish.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import "./highlight";
import { setBasePath } from "@moq/hang-ui/utilities";

setBasePath(__HANG_UI_ASSETS_PATH__);

import "@moq/hang-ui/publish/element";

// We need to import Web Components with fully-qualified paths because of tree-shaking.
Expand Down
2 changes: 2 additions & 0 deletions js/hang-demo/src/vite-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ interface ImportMetaEnv {
interface ImportMeta {
readonly env: ImportMetaEnv;
}

declare const __HANG_UI_ASSETS_PATH__: string;
5 changes: 5 additions & 0 deletions js/hang-demo/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import tailwindcss from "@tailwindcss/vite";
import path from "path";
import { defineConfig } from "vite";
import solidPlugin from "vite-plugin-solid";

export default defineConfig({
root: "src",
plugins: [tailwindcss(), solidPlugin()],
define: {
// Inject the hang-ui assets path for development
__HANG_UI_ASSETS_PATH__: JSON.stringify(`/@fs${path.resolve(__dirname, "../hang-ui/dist")}`),
},
build: {
target: "esnext",
sourcemap: process.env.NODE_ENV === "production" ? false : "inline",
Expand Down
68 changes: 68 additions & 0 deletions js/hang-ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,71 @@ Here's how you can use them (see also @moq/hang-demo for a complete example):
</hang-publish>
</hang-publish-ui>
```

## Asset Configuration
The hang-ui library loads assets (icons, CSS stylesheets) at runtime from a base path. By default, the library auto-detects the base path by inspecting the script tag that loaded the library. However, you can configure this manually if needed.

### Auto-Detection (Default)
When you include the hang-ui library via a script tag or module, it automatically detects where it was loaded from:

```html
<script type="module" src="/node_modules/@moq/hang-ui/dist/index.js"></script>
```

The library will use `/node_modules/@moq/hang-ui/dist` as the base path for loading assets.

### Manual Configuration with `setBasePath`
If you're serving assets from a different location (CDN, custom build, etc.), you should call `setBasePath()` before any components are initialized:

```typescript
import { setBasePath } from '@moq/hang-ui';

// For npm users with a custom asset location
setBasePath('/assets/hang-ui');

// For CDN users
setBasePath('https://cdn.example.com/hang-ui/v0.1.0');

// For development with copied assets
setBasePath('/public/hang-ui');
```

**Important:** Call `setBasePath()` before initializing any hang-ui components to ensure assets load correctly.

### Asset Structure
When using `setBasePath`, ensure your asset directory contains the required files. The library expects the following structure in the dist folder:

```plaintext
your-base-path/
├── assets/
│ └── icons/
│ └── *.svg (icon files)
└── themes/
├── watch/
│ └── styles.css
├── publish/
│ └── styles.css
└── *.css (shared theme files)
```

If you installed via npm, these assets are already included in `node_modules/@moq/hang-ui/dist/`. If you're using a CDN or copying the library, make sure to include the entire `dist` folder with all assets.

### Utility Functions
The library exports several utility functions for asset management:

- **`setBasePath(path: string)`**: Override the auto-detected base path for assets
- **`getBasePath(subpath?: string)`**: Get the base path or construct a full asset URL
- **`whenBasePathReady()`**: Wait for the base path to be set (used internally by components)

## Development
### Building
```bash
bun run build
```

This will compile the TypeScript code and copy all assets (icons and themes) to the `dist` folder.

### Testing
```bash
bun run test
```
7 changes: 6 additions & 1 deletion js/hang-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@
"exports": {
"./publish/element": "./src/Components/publish/element.tsx",
"./watch/element": "./src/Components/watch/element.tsx",
"./stats": "./src/Components/stats/index.ts"
"./stats": "./src/Components/stats/index.ts",
"./utilities": "./src/utilities/index.ts"
},
"sideEffects": [
"./src/Components/publish/element.tsx",
"./src/Components/watch/element.tsx",
"./src/Components/stats/element.ts"
],
"scripts": {
"prepare": "bun run build",
"build": "bun run clean && rollup -c && bun ../scripts/package.ts",
"check": "tsc --noEmit",
"clean": "rimraf dist",
Expand All @@ -29,9 +31,12 @@
},
"devDependencies": {
"@rollup/plugin-node-resolve": "^16.0.3",
"@solidjs/testing-library": "^0.8.10",
"@testing-library/jest-dom": "^6.9.1",
"happy-dom": "^20.0.11",
"rimraf": "^6.0.1",
"rollup": "^4.53.3",
"rollup-plugin-copy": "^3.5.0",
"rollup-plugin-esbuild": "^6.2.1",
"solid-element": "^1.9.1",
"solid-js": "^1.9.10",
Expand Down
51 changes: 33 additions & 18 deletions js/hang-ui/rollup.config.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { readFileSync } from "node:fs";
import nodeResolve from "@rollup/plugin-node-resolve";
import copy from "rollup-plugin-copy";
import esbuild from "rollup-plugin-esbuild";
import solid from "unplugin-solid/rollup";

Expand All @@ -16,41 +17,55 @@ function inlineCss() {
};
}

// Shared plugins used by all build configs
const sharedPlugins = [
inlineCss(),
solid({ dev: false, hydratable: false }),
esbuild({
include: /\.[jt]sx?$/,
jsx: "preserve",
tsconfig: "tsconfig.json",
}),
nodeResolve({ extensions: [".js", ".ts", ".tsx"] }),
];

export default [
{
input: "src/Components/publish/element.tsx",
input: "src/utilities/index.ts",
output: {
file: "dist/publish-controls.esm.js",
file: "dist/utilities/index.js",
format: "es",
sourcemap: true,
},
plugins: [
inlineCss(),
solid({ dev: false, hydratable: false }),
esbuild({
include: /\.[jt]sx?$/,
jsx: "preserve",
include: /\.[jt]s$/,
tsconfig: "tsconfig.json",
}),
nodeResolve({ extensions: [".js", ".ts", ".tsx"] }),
copy({
targets: [
{ src: "src/assets/", dest: "dist/" },
{ src: "src/themes/", dest: "dist/" },
],
copyOnce: true,
}),
],
},
{
input: "src/Components/publish/element.tsx",
output: {
file: "dist/publish-controls.esm.js",
format: "es",
sourcemap: true,
},
plugins: sharedPlugins,
},
{
input: "src/Components/watch/element.tsx",
output: {
file: "dist/watch-controls.esm.js",
format: "es",
sourcemap: true,
},
plugins: [
inlineCss(),
solid({ dev: false, hydratable: false }),
esbuild({
include: /\.[jt]sx?$/,
jsx: "preserve",
tsconfig: "tsconfig.json",
}),
nodeResolve({ extensions: [".js", ".ts", ".tsx"] }),
],
plugins: sharedPlugins,
},
];
11 changes: 6 additions & 5 deletions js/hang-ui/src/Components/publish/CameraSourceButton.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Show } from "solid-js";
import Button from "../shared/components/button";
import Icon from "../shared/components/icon";
import MediaSourceSourceSelector from "./MediaSourceSelector";
import usePublishUIContext from "./usePublishUIContext";

Expand All @@ -23,14 +25,13 @@ export default function CameraSourceButton() {

return (
<div class="publishSourceButtonContainer">
<button
type="button"
<Button
title="Camera"
class={`publishButton publishSourceButton ${context.cameraActive() ? "active" : ""}`}
class={`publishSourceButton ${context.cameraActive() ? "active" : ""}`}
onClick={onClick}
>
📷
</button>
<Icon name="camera" />
</Button>
<Show when={context.cameraActive() && context.cameraDevices().length}>
<MediaSourceSourceSelector
sources={context.cameraDevices()}
Expand Down
11 changes: 6 additions & 5 deletions js/hang-ui/src/Components/publish/FileSourceButton.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { createSignal } from "solid-js";
import Button from "../shared/components/button";
import Icon from "../shared/components/icon";
import usePublishUIContext from "./usePublishUIContext";

export default function FileSourceButton() {
Expand All @@ -24,14 +26,13 @@ export default function FileSourceButton() {
class="hidden"
accept="video/*,audio/*,image/*"
/>
<button
type="button"
<Button
title="Upload File"
class={`publishSourceButton ${context.fileActive() ? "active" : ""}`}
onClick={onClick}
class={`publishButton publishSourceButton ${context.fileActive() ? "active" : ""}`}
>
📁
</button>
<Icon name="file" />
</Button>
</>
);
}
11 changes: 6 additions & 5 deletions js/hang-ui/src/Components/publish/MediaSourceSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { createSignal, For, Show } from "solid-js";
import Button from "../shared/components/button";
import Icon from "../shared/components/icon";

type MediaSourceSelectorProps = {
sources?: MediaDeviceInfo[];
Expand All @@ -13,14 +15,13 @@ export default function MediaSourceSelector(props: MediaSourceSelectorProps) {

return (
<>
<button
type="button"
<Button
onClick={toggleSourcesVisible}
class="publishButton mediaSourceVisibilityToggle"
class="mediaSourceVisibilityToggle button--media-source-selector"
title={sourcesVisible() ? "Hide Sources" : "Show Sources"}
>
{sourcesVisible() ? "" : "▼"}
</button>
<Icon name={sourcesVisible() ? "arrow-up" : "arrow-down"} />
</Button>
<Show when={sourcesVisible()}>
<select
value={props.selectedSource}
Expand Down
11 changes: 6 additions & 5 deletions js/hang-ui/src/Components/publish/MicrophoneSourceButton.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Show } from "solid-js";
import Button from "../shared/components/button";
import Icon from "../shared/components/icon";
import MediaSourceSourceSelector from "./MediaSourceSelector";
import usePublishUIContext from "./usePublishUIContext";

Expand All @@ -23,14 +25,13 @@ export default function MicrophoneSourceButton() {

return (
<div class="publishSourceButtonContainer">
<button
type="button"
<Button
title="Microphone"
class={`publishButton publishSourceButton ${context.microphoneActive() ? "active" : ""}`}
class={`publishSourceButton ${context.microphoneActive() ? "active" : ""}`}
onClick={onClick}
>
🎤
</button>
<Icon name="microphone" />
</Button>
<Show when={context.microphoneActive() && context.microphoneDevices().length}>
<MediaSourceSourceSelector
sources={context.microphoneDevices()}
Expand Down
11 changes: 6 additions & 5 deletions js/hang-ui/src/Components/publish/NothingSourceButton.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import Button from "../shared/components/button";
import Icon from "../shared/components/icon";
import usePublishUIContext from "./usePublishUIContext";

export default function NothingSourceButton() {
Expand All @@ -10,14 +12,13 @@ export default function NothingSourceButton() {

return (
<div class="publishSourceButtonContainer">
<button
type="button"
<Button
title="No Source"
class={`publishButton publishSourceButton ${context.nothingActive() ? "active" : ""}`}
class={`publishSourceButton ${context.nothingActive() ? "active" : ""}`}
onClick={onClick}
>
🚫
</button>
<Icon name="ban" />
</Button>
</div>
);
}
Loading
Loading