@jxsuite/ui 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +11 -0
- package/components/jx-action-button.json +414 -0
- package/components/jx-button.json +310 -0
- package/components/jx-icon.json +81 -0
- package/components/jx-menu-item.json +265 -0
- package/components/jx-menu.json +122 -0
- package/icons/list.json +102 -0
- package/icons/manifest.json +348 -0
- package/package.json +46 -0
- package/project.json +167 -0
- package/src/behaviors/menu.ts +396 -0
- package/src/documents.ts +23 -0
- package/src/icons-build.ts +74 -0
- package/src/icons.ts +55 -0
- package/src/index.ts +70 -0
- package/src/testing/popover-shim.ts +139 -0
- package/src/theme.ts +63 -0
- package/stylebook/jx-action-button.json +108 -0
- package/stylebook/jx-button.json +104 -0
- package/stylebook/jx-icon.json +208 -0
- package/stylebook/jx-menu-item.json +221 -0
- package/stylebook/jx-menu.json +302 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pure half of the icon build: how a Phosphor asset is named, and how its path data is lifted
|
|
3
|
+
* out of the SVG. `scripts/build-icons.ts` supplies the file system; this module can be tested with
|
|
4
|
+
* strings.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** The three weights the kit ships. `fill` is the "active" drawing of the same glyph. */
|
|
8
|
+
export const ICON_WEIGHTS = ["regular", "bold", "fill"] as const;
|
|
9
|
+
export type IconWeight = (typeof ICON_WEIGHTS)[number];
|
|
10
|
+
|
|
11
|
+
/** `name → weights` as authored in `icons/list.json`. */
|
|
12
|
+
export type IconList = Record<string, IconWeight[]>;
|
|
13
|
+
|
|
14
|
+
/** The committed manifest: one path-data string per name and weight, on one shared viewBox. */
|
|
15
|
+
export interface IconManifest {
|
|
16
|
+
viewBox: string;
|
|
17
|
+
icons: Record<string, Partial<Record<IconWeight, string>>>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The Phosphor viewBox every glyph is drawn on. */
|
|
21
|
+
export const ICON_VIEW_BOX = "0 0 256 256";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The asset path inside `@phosphor-icons/core` for a name and weight. Regular glyphs carry no
|
|
25
|
+
* suffix; every other weight is `<name>-<weight>.svg` under its own directory.
|
|
26
|
+
*/
|
|
27
|
+
export function assetPath(name: string, weight: IconWeight): string {
|
|
28
|
+
return weight === "regular"
|
|
29
|
+
? `assets/regular/${name}.svg`
|
|
30
|
+
: `assets/${weight}/${name}-${weight}.svg`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const PATH_D = /<path\b[^>]*\bd="([^"]+)"/g;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Every `d` attribute in an SVG, joined into one path-data string. A glyph is one `<path>` in
|
|
37
|
+
* practice; a second path starts with its own `M`, so concatenation draws both.
|
|
38
|
+
*
|
|
39
|
+
* @throws {Error} When the SVG carries no path at all — an icon that draws nothing is a build
|
|
40
|
+
* error.
|
|
41
|
+
*/
|
|
42
|
+
export function extractPathData(svg: string): string {
|
|
43
|
+
const parts: string[] = [];
|
|
44
|
+
for (const match of svg.matchAll(PATH_D)) {
|
|
45
|
+
parts.push(match[1]!);
|
|
46
|
+
}
|
|
47
|
+
if (parts.length === 0) {
|
|
48
|
+
throw new Error("icon svg carries no <path d>");
|
|
49
|
+
}
|
|
50
|
+
return parts.join(" ");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Build the manifest from the list, reading each asset through `read`. Names and weights are
|
|
55
|
+
* emitted sorted, so the committed file is stable across runs.
|
|
56
|
+
*/
|
|
57
|
+
export function buildManifest(
|
|
58
|
+
list: IconList,
|
|
59
|
+
read: (relativePath: string) => string,
|
|
60
|
+
): IconManifest {
|
|
61
|
+
const icons: IconManifest["icons"] = {};
|
|
62
|
+
for (const name of Object.keys(list).toSorted()) {
|
|
63
|
+
const weights = list[name]!;
|
|
64
|
+
const entry: Partial<Record<IconWeight, string>> = {};
|
|
65
|
+
for (const weight of ICON_WEIGHTS) {
|
|
66
|
+
if (!weights.includes(weight)) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
entry[weight] = extractPathData(read(assetPath(name, weight)));
|
|
70
|
+
}
|
|
71
|
+
icons[name] = entry;
|
|
72
|
+
}
|
|
73
|
+
return { icons, viewBox: ICON_VIEW_BOX };
|
|
74
|
+
}
|
package/src/icons.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Icon lookup over the committed manifest. `jx-icon` reaches this through `$src`, so the manifest
|
|
3
|
+
* stays a plain import rather than a state entry: a hundred kilobytes of path data has no business
|
|
4
|
+
* being a reactive object.
|
|
5
|
+
*
|
|
6
|
+
* @docs extending/ui-kit
|
|
7
|
+
*/
|
|
8
|
+
import manifest from "../icons/manifest.json";
|
|
9
|
+
import { ICON_VIEW_BOX, ICON_WEIGHTS } from "./icons-build.ts";
|
|
10
|
+
import type { IconManifest, IconWeight } from "./icons-build.ts";
|
|
11
|
+
|
|
12
|
+
export { ICON_VIEW_BOX, ICON_WEIGHTS } from "./icons-build.ts";
|
|
13
|
+
export type { IconWeight } from "./icons-build.ts";
|
|
14
|
+
|
|
15
|
+
const { icons } = manifest as IconManifest;
|
|
16
|
+
|
|
17
|
+
/** Every icon name the kit ships, sorted. */
|
|
18
|
+
export const ICON_NAMES: readonly string[] = Object.keys(icons);
|
|
19
|
+
|
|
20
|
+
/** Whether the manifest carries a glyph for the name, at the weight when one is given. */
|
|
21
|
+
export function hasIcon(name: string, weight?: IconWeight): boolean {
|
|
22
|
+
const entry = icons[name];
|
|
23
|
+
if (!entry) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
return weight === undefined || weight in entry;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const warned = new Set<string>();
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The path data for a name and weight. A weight the manifest lacks falls back to `regular`, which
|
|
33
|
+
* every listed icon carries; a name it lacks warns once and draws nothing, so a typo shows as a gap
|
|
34
|
+
* rather than a crash or a wrong glyph.
|
|
35
|
+
*
|
|
36
|
+
* Called from the `jx-icon` document with the element's own `name` and `weight` state.
|
|
37
|
+
*/
|
|
38
|
+
export function iconPath(name: unknown, weight: unknown): string {
|
|
39
|
+
const key = typeof name === "string" ? name : "";
|
|
40
|
+
const entry = icons[key];
|
|
41
|
+
if (!entry) {
|
|
42
|
+
if (key && !warned.has(key)) {
|
|
43
|
+
warned.add(key);
|
|
44
|
+
console.warn(`jx-icon: no icon named "${key}"`);
|
|
45
|
+
}
|
|
46
|
+
return "";
|
|
47
|
+
}
|
|
48
|
+
const w = (ICON_WEIGHTS as readonly string[]).includes(String(weight))
|
|
49
|
+
? (weight as IconWeight)
|
|
50
|
+
: "regular";
|
|
51
|
+
return entry[w] ?? entry.regular ?? "";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The viewBox `jx-icon` draws on; the manifest's, so a rebuilt manifest cannot drift from it. */
|
|
55
|
+
export const iconViewBox: string = (manifest as IconManifest).viewBox || ICON_VIEW_BOX;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@jxsuite/ui` — register the kit's elements into the current realm, with no network.
|
|
3
|
+
*
|
|
4
|
+
* The kit ships JSON sources, not compiled classes. `registerUi()` preloads every document under
|
|
5
|
+
* its `jx-ui:/components/<tag>.json` URL and every behaviour module under the specifier the
|
|
6
|
+
* documents spell in `$src`, then defines each element through the runtime's own `defineElement`. A
|
|
7
|
+
* document's `$elements` and `$src` therefore resolve exactly as they would from a server, and the
|
|
8
|
+
* bundler saw every file (embedding.md §6).
|
|
9
|
+
*
|
|
10
|
+
* @docs extending/ui-kit
|
|
11
|
+
*/
|
|
12
|
+
import { defineElement, preloadDocument, preloadModule } from "@jxsuite/runtime";
|
|
13
|
+
import * as menu from "./behaviors/menu.ts";
|
|
14
|
+
import { documents } from "./documents.ts";
|
|
15
|
+
import * as icons from "./icons.ts";
|
|
16
|
+
import { installTheme } from "./theme.ts";
|
|
17
|
+
|
|
18
|
+
export { documents, KIT_TAGS } from "./documents.ts";
|
|
19
|
+
export { hasIcon, ICON_NAMES, ICON_WEIGHTS, iconPath, iconViewBox } from "./icons.ts";
|
|
20
|
+
export type { IconWeight } from "./icons.ts";
|
|
21
|
+
export {
|
|
22
|
+
installTheme,
|
|
23
|
+
THEME_LAYER,
|
|
24
|
+
themeCSS,
|
|
25
|
+
themeInstalled,
|
|
26
|
+
themeTokenNames,
|
|
27
|
+
themeTokens,
|
|
28
|
+
} from "./theme.ts";
|
|
29
|
+
|
|
30
|
+
/** The base every kit document resolves its references against. */
|
|
31
|
+
export const KIT_BASE = "jx-ui:/components/";
|
|
32
|
+
|
|
33
|
+
/** Behaviour modules, under the `$src` specifiers the documents use. */
|
|
34
|
+
export const KIT_MODULES: Readonly<Record<string, Record<string, unknown>>> = {
|
|
35
|
+
"jx-ui:/behaviors/menu.ts": menu,
|
|
36
|
+
"jx-ui:/icons.ts": icons,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export interface RegisterUiOptions {
|
|
40
|
+
/** Adopt the theme into the document as well. Default is true. */
|
|
41
|
+
theme?: boolean;
|
|
42
|
+
/** The document to adopt the theme into. Default is the global document. */
|
|
43
|
+
document?: Document;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let registration: Promise<void> | null = null;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Define every kit element in this realm, once. Idempotent: later calls return the same promise.
|
|
50
|
+
*
|
|
51
|
+
* @param {RegisterUiOptions} [options]
|
|
52
|
+
* @returns {Promise<void>} Settles when every element is defined
|
|
53
|
+
*/
|
|
54
|
+
export function registerUi(options: RegisterUiOptions = {}): Promise<void> {
|
|
55
|
+
if (options.theme !== false) {
|
|
56
|
+
installTheme(options.document);
|
|
57
|
+
}
|
|
58
|
+
registration ??= (async () => {
|
|
59
|
+
for (const [specifier, mod] of Object.entries(KIT_MODULES)) {
|
|
60
|
+
preloadModule(specifier, mod);
|
|
61
|
+
}
|
|
62
|
+
for (const [tag, doc] of Object.entries(documents)) {
|
|
63
|
+
preloadDocument(`${KIT_BASE}${tag}.json`, doc);
|
|
64
|
+
}
|
|
65
|
+
for (const doc of Object.values(documents)) {
|
|
66
|
+
await defineElement(doc, KIT_BASE);
|
|
67
|
+
}
|
|
68
|
+
})();
|
|
69
|
+
return registration;
|
|
70
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What happy-dom 20 lacks and the kit's overlays are built on: the popover API. A test-only shim,
|
|
3
|
+
* exported as `@jxsuite/ui/testing/popover-shim` so a host's own DOM harness installs the same
|
|
4
|
+
* one.
|
|
5
|
+
*
|
|
6
|
+
* `showPopover()` / `hidePopover()` / `togglePopover()` are defined on `HTMLElement.prototype` only
|
|
7
|
+
* when missing. The shim keeps the parts of the standard's algorithm the kit relies on — a
|
|
8
|
+
* `popover` attribute is required, `beforetoggle` fires synchronously and `toggle` in a microtask
|
|
9
|
+
* with `oldState`/`newState`, showing an `auto` popover light-dismisses unrelated `auto` popovers,
|
|
10
|
+
* hiding one hides the popovers nested inside it, a mousedown outside or an Escape hides the
|
|
11
|
+
* topmost `auto` one — and marks a showing popover with `data-popover-open`, because
|
|
12
|
+
* `:popover-open` never matches here. Test-only: paint, the top layer and real focus restoration
|
|
13
|
+
* are the browser's to verify.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
interface PopoverState {
|
|
17
|
+
open: Set<HTMLElement>;
|
|
18
|
+
/** The invoker each showing popover was shown from, when `showPopover({ source })` named one. */
|
|
19
|
+
invokers: WeakMap<HTMLElement, Element>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
declare global {
|
|
23
|
+
interface Window {
|
|
24
|
+
__jxPopoverShim?: PopoverState;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function toggleEvent(type: "beforetoggle" | "toggle", oldState: string, newState: string): Event {
|
|
29
|
+
const event = new Event(type, { bubbles: false, cancelable: type === "beforetoggle" });
|
|
30
|
+
Object.assign(event, { newState, oldState });
|
|
31
|
+
return event;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isAuto(el: HTMLElement): boolean {
|
|
35
|
+
return el.getAttribute("popover") !== "manual";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function installPopoverShim(): void {
|
|
39
|
+
const proto = HTMLElement.prototype as HTMLElement & {
|
|
40
|
+
showPopover?: (options?: { source?: Element }) => void;
|
|
41
|
+
hidePopover?: () => void;
|
|
42
|
+
togglePopover?: (force?: boolean) => boolean;
|
|
43
|
+
};
|
|
44
|
+
if (typeof proto.showPopover === "function") {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const state: PopoverState = { invokers: new WeakMap(), open: new Set() };
|
|
48
|
+
window.__jxPopoverShim = state;
|
|
49
|
+
|
|
50
|
+
const hide = (el: HTMLElement): void => {
|
|
51
|
+
if (!state.open.has(el)) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
for (const other of [...state.open].toReversed()) {
|
|
55
|
+
if (other !== el && el.contains(other)) {
|
|
56
|
+
hide(other);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
el.dispatchEvent(toggleEvent("beforetoggle", "open", "closed"));
|
|
60
|
+
state.open.delete(el);
|
|
61
|
+
delete el.dataset.popoverOpen;
|
|
62
|
+
queueMicrotask(() => el.dispatchEvent(toggleEvent("toggle", "open", "closed")));
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
proto.showPopover = function showPopover(
|
|
66
|
+
this: HTMLElement,
|
|
67
|
+
options?: { source?: Element },
|
|
68
|
+
): void {
|
|
69
|
+
if (!this.hasAttribute("popover")) {
|
|
70
|
+
throw new DOMException("Not a popover element", "NotSupportedError");
|
|
71
|
+
}
|
|
72
|
+
if (state.open.has(this)) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
// An invoker is not "outside": a mousedown on the button that opened a popover leaves it to
|
|
76
|
+
// The button's own click, which is what makes a toggle a toggle.
|
|
77
|
+
if (options?.source) {
|
|
78
|
+
state.invokers.set(this, options.source);
|
|
79
|
+
} else {
|
|
80
|
+
state.invokers.delete(this);
|
|
81
|
+
}
|
|
82
|
+
if (isAuto(this)) {
|
|
83
|
+
const showing = [...state.open];
|
|
84
|
+
for (const other of showing) {
|
|
85
|
+
if (isAuto(other) && !other.contains(this)) {
|
|
86
|
+
hide(other);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
this.dispatchEvent(toggleEvent("beforetoggle", "closed", "open"));
|
|
91
|
+
state.open.add(this);
|
|
92
|
+
this.dataset.popoverOpen = "";
|
|
93
|
+
queueMicrotask(() => this.dispatchEvent(toggleEvent("toggle", "closed", "open")));
|
|
94
|
+
};
|
|
95
|
+
proto.hidePopover = function hidePopover(this: HTMLElement): void {
|
|
96
|
+
if (!this.hasAttribute("popover")) {
|
|
97
|
+
throw new DOMException("Not a popover element", "NotSupportedError");
|
|
98
|
+
}
|
|
99
|
+
hide(this);
|
|
100
|
+
};
|
|
101
|
+
proto.togglePopover = function togglePopover(this: HTMLElement, force?: boolean): boolean {
|
|
102
|
+
const show = force ?? !state.open.has(this);
|
|
103
|
+
if (show) {
|
|
104
|
+
this.showPopover!();
|
|
105
|
+
} else {
|
|
106
|
+
this.hidePopover!();
|
|
107
|
+
}
|
|
108
|
+
return show;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
document.addEventListener(
|
|
112
|
+
"mousedown",
|
|
113
|
+
(event) => {
|
|
114
|
+
for (const el of [...state.open].toReversed()) {
|
|
115
|
+
const target = event.target instanceof Node ? event.target : null;
|
|
116
|
+
const inside =
|
|
117
|
+
target !== null &&
|
|
118
|
+
(el.contains(target) || state.invokers.get(el)?.contains(target) === true);
|
|
119
|
+
if (isAuto(el) && !inside) {
|
|
120
|
+
hide(el);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
true,
|
|
125
|
+
);
|
|
126
|
+
document.addEventListener(
|
|
127
|
+
"keydown",
|
|
128
|
+
(event) => {
|
|
129
|
+
if (event.key !== "Escape") {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const top = [...state.open].toReversed().find((el) => isAuto(el));
|
|
133
|
+
if (top) {
|
|
134
|
+
hide(top);
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
true,
|
|
138
|
+
);
|
|
139
|
+
}
|
package/src/theme.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The kit's theme: one stylesheet of design tokens, built from the kit's own `project.json`.
|
|
3
|
+
*
|
|
4
|
+
* The tokens live in the project's `style` block, exactly where a site keeps its design tokens
|
|
5
|
+
* (site-architecture.md §10.2), so opening the kit as a project in Studio shows the same values the
|
|
6
|
+
* shell runs on. This module turns that block into CSS with the runtime's own style builder and
|
|
7
|
+
* wraps it in `@layer jx-ui`, so any unlayered author rule — a site's override, Studio's chrome —
|
|
8
|
+
* wins over a token by cascade order rather than by specificity.
|
|
9
|
+
*
|
|
10
|
+
* @docs extending/ui-kit
|
|
11
|
+
*/
|
|
12
|
+
import { buildStyleRules } from "@jxsuite/runtime/css";
|
|
13
|
+
import type { JxStyle } from "@jxsuite/schema/types";
|
|
14
|
+
import project from "../project.json";
|
|
15
|
+
|
|
16
|
+
/** The cascade layer every kit stylesheet lives in. */
|
|
17
|
+
export const THEME_LAYER = "jx-ui";
|
|
18
|
+
|
|
19
|
+
/** The token block as authored: `project.json`'s `style`. */
|
|
20
|
+
export const themeTokens = project.style as JxStyle;
|
|
21
|
+
|
|
22
|
+
/** The theme as CSS text: every token on `:root`, inside `@layer jx-ui`. */
|
|
23
|
+
export function themeCSS(): string {
|
|
24
|
+
const rules = buildStyleRules(themeTokens, { scope: ":root" }).map((rule) => rule.text);
|
|
25
|
+
return `@layer ${THEME_LAYER} {\n${rules.join("\n")}\n}\n`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The custom-property names the theme declares at the root, in declaration order. */
|
|
29
|
+
export function themeTokenNames(): string[] {
|
|
30
|
+
return Object.keys(themeTokens).filter((key) => key.startsWith("--"));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const installed = new WeakMap<Document, CSSStyleSheet | HTMLStyleElement>();
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Adopt the theme into a document, once. A constructable sheet where the host has one, a `<style>`
|
|
37
|
+
* element otherwise; calling it again for the same document is a no-op.
|
|
38
|
+
*
|
|
39
|
+
* @param {Document} [doc] Default is the global document
|
|
40
|
+
*/
|
|
41
|
+
export function installTheme(doc: Document = document): void {
|
|
42
|
+
if (installed.has(doc)) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const css = themeCSS();
|
|
46
|
+
if (typeof CSSStyleSheet === "function" && "replaceSync" in CSSStyleSheet.prototype) {
|
|
47
|
+
const sheet = new CSSStyleSheet();
|
|
48
|
+
sheet.replaceSync(css);
|
|
49
|
+
doc.adoptedStyleSheets = [...doc.adoptedStyleSheets, sheet];
|
|
50
|
+
installed.set(doc, sheet);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const style = doc.createElement("style");
|
|
54
|
+
style.dataset.jxTheme = THEME_LAYER;
|
|
55
|
+
style.textContent = css;
|
|
56
|
+
doc.head.append(style);
|
|
57
|
+
installed.set(doc, style);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Whether {@link installTheme} has run for a document. */
|
|
61
|
+
export function themeInstalled(doc: Document = document): boolean {
|
|
62
|
+
return installed.has(doc);
|
|
63
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://jxsuite.com/schema/v1",
|
|
3
|
+
"$id": "StylebookActionButton",
|
|
4
|
+
"tagName": "main",
|
|
5
|
+
"$elements": [
|
|
6
|
+
{ "$ref": "../components/jx-icon.json" },
|
|
7
|
+
{ "$ref": "../components/jx-action-button.json" }
|
|
8
|
+
],
|
|
9
|
+
"state": {
|
|
10
|
+
"bold": false,
|
|
11
|
+
"note": {
|
|
12
|
+
"$prototype": "Function",
|
|
13
|
+
"parameters": ["event"],
|
|
14
|
+
"body": [
|
|
15
|
+
{
|
|
16
|
+
"operator": "=",
|
|
17
|
+
"target": { "$ref": "#/state/bold" },
|
|
18
|
+
"value": { "$ref": "event#/detail" }
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"style": {
|
|
24
|
+
"fontFamily": "var(--jx-font-sans)",
|
|
25
|
+
"fontSize": "var(--jx-text-md)",
|
|
26
|
+
"color": "var(--jx-fg)",
|
|
27
|
+
"background": "var(--jx-bg)",
|
|
28
|
+
"padding": "var(--jx-space-6)",
|
|
29
|
+
"minHeight": "100vh",
|
|
30
|
+
"& > h1": {
|
|
31
|
+
"fontSize": "var(--jx-text-lg)",
|
|
32
|
+
"fontWeight": "600",
|
|
33
|
+
"margin": "0 0 var(--jx-space-5)"
|
|
34
|
+
},
|
|
35
|
+
"& > p": { "color": "var(--jx-fg-dim)", "margin": "0 0 var(--jx-space-5)" },
|
|
36
|
+
"& [role=\"toolbar\"]": {
|
|
37
|
+
"display": "flex",
|
|
38
|
+
"alignItems": "center",
|
|
39
|
+
"gap": "var(--jx-space-1)",
|
|
40
|
+
"padding": "var(--jx-space-1)",
|
|
41
|
+
"background": "var(--jx-bg-panel)",
|
|
42
|
+
"border": "1px solid var(--jx-border)",
|
|
43
|
+
"borderRadius": "var(--jx-radius-md)",
|
|
44
|
+
"width": "fit-content",
|
|
45
|
+
"margin": "0 0 var(--jx-space-4)"
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"children": [
|
|
49
|
+
{ "tagName": "h1", "textContent": "jx-action-button" },
|
|
50
|
+
{
|
|
51
|
+
"tagName": "p",
|
|
52
|
+
"textContent": "Icon-first tool buttons: quiet in a toolbar, a toggle with aria-pressed, an emphasized selection, and a text-and-icon form. Bold is ${state.bold ? 'on' : 'off'}."
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"tagName": "div",
|
|
56
|
+
"attributes": { "role": "toolbar", "aria-label": "Formatting" },
|
|
57
|
+
"children": [
|
|
58
|
+
{
|
|
59
|
+
"tagName": "jx-action-button",
|
|
60
|
+
"$props": { "label": "Bold", "icon": "text-b", "toggles": true },
|
|
61
|
+
"onchange": { "$ref": "#/state/note" }
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"tagName": "jx-action-button",
|
|
65
|
+
"$props": { "label": "Italic", "icon": "text-italic", "toggles": true, "selected": true }
|
|
66
|
+
},
|
|
67
|
+
{ "tagName": "jx-action-button", "$props": { "label": "Link", "icon": "link" } },
|
|
68
|
+
{
|
|
69
|
+
"tagName": "jx-action-button",
|
|
70
|
+
"$props": { "label": "Undo", "icon": "arrow-u-up-left", "disabled": true }
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"tagName": "div",
|
|
76
|
+
"attributes": { "role": "toolbar", "aria-label": "Emphasized and sized" },
|
|
77
|
+
"children": [
|
|
78
|
+
{
|
|
79
|
+
"tagName": "jx-action-button",
|
|
80
|
+
"$props": {
|
|
81
|
+
"label": "Selected, emphasized",
|
|
82
|
+
"icon": "eye",
|
|
83
|
+
"toggles": true,
|
|
84
|
+
"selected": true,
|
|
85
|
+
"emphasized": true
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"tagName": "jx-action-button",
|
|
90
|
+
"$props": { "label": "Not quiet", "icon": "gear", "quiet": false }
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"tagName": "jx-action-button",
|
|
94
|
+
"$props": { "label": "Small", "icon": "plus", "size": "sm" }
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"tagName": "jx-action-button",
|
|
98
|
+
"$props": { "label": "Large", "icon": "plus", "size": "lg" }
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"tagName": "jx-action-button",
|
|
102
|
+
"$props": { "label": "Save", "icon": "floppy-disk", "quiet": false },
|
|
103
|
+
"children": [{ "tagName": "span", "textContent": "Save" }]
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
}
|
|
107
|
+
]
|
|
108
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://jxsuite.com/schema/v1",
|
|
3
|
+
"$id": "StylebookButton",
|
|
4
|
+
"tagName": "main",
|
|
5
|
+
"$elements": [
|
|
6
|
+
{ "$ref": "../components/jx-icon.json" },
|
|
7
|
+
{ "$ref": "../components/jx-button.json" }
|
|
8
|
+
],
|
|
9
|
+
"state": {
|
|
10
|
+
"variants": ["accent", "primary", "secondary", "negative"],
|
|
11
|
+
"clicks": 0,
|
|
12
|
+
"count": {
|
|
13
|
+
"$prototype": "Function",
|
|
14
|
+
"body": [{ "operator": "+=", "target": { "$ref": "#/state/clicks" }, "value": 1 }]
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"style": {
|
|
18
|
+
"fontFamily": "var(--jx-font-sans)",
|
|
19
|
+
"fontSize": "var(--jx-text-md)",
|
|
20
|
+
"color": "var(--jx-fg)",
|
|
21
|
+
"background": "var(--jx-bg)",
|
|
22
|
+
"padding": "var(--jx-space-6)",
|
|
23
|
+
"minHeight": "100vh",
|
|
24
|
+
"& > h1": {
|
|
25
|
+
"fontSize": "var(--jx-text-lg)",
|
|
26
|
+
"fontWeight": "600",
|
|
27
|
+
"margin": "0 0 var(--jx-space-5)"
|
|
28
|
+
},
|
|
29
|
+
"& > p": { "color": "var(--jx-fg-dim)", "margin": "0 0 var(--jx-space-5)" },
|
|
30
|
+
"& [role=\"group\"]": {
|
|
31
|
+
"display": "flex",
|
|
32
|
+
"alignItems": "center",
|
|
33
|
+
"gap": "var(--jx-space-3)",
|
|
34
|
+
"margin": "0 0 var(--jx-space-4)"
|
|
35
|
+
},
|
|
36
|
+
"& [role=\"group\"] > span": {
|
|
37
|
+
"width": "80px",
|
|
38
|
+
"color": "var(--jx-fg-muted)",
|
|
39
|
+
"fontSize": "var(--jx-text-sm)"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"children": [
|
|
43
|
+
{ "tagName": "h1", "textContent": "jx-button" },
|
|
44
|
+
{
|
|
45
|
+
"tagName": "p",
|
|
46
|
+
"textContent": "Every variant in every state. The accent is spent once per surface; secondary is the default. Clicked ${state.clicks} times."
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"$prototype": "Array",
|
|
50
|
+
"items": { "$ref": "#/state/variants" },
|
|
51
|
+
"key": { "$ref": "$map/item" },
|
|
52
|
+
"map": {
|
|
53
|
+
"tagName": "div",
|
|
54
|
+
"attributes": { "role": "group", "aria-label": "${$map.item} buttons" },
|
|
55
|
+
"children": [
|
|
56
|
+
{ "tagName": "span", "textContent": "${$map.item}" },
|
|
57
|
+
{
|
|
58
|
+
"tagName": "jx-button",
|
|
59
|
+
"$props": { "variant": { "$ref": "$map/item" } },
|
|
60
|
+
"onclick": { "$ref": "#/state/count" },
|
|
61
|
+
"children": [{ "tagName": "span", "textContent": "Label" }]
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"tagName": "jx-button",
|
|
65
|
+
"$props": { "variant": { "$ref": "$map/item" } },
|
|
66
|
+
"children": [
|
|
67
|
+
{
|
|
68
|
+
"tagName": "jx-icon",
|
|
69
|
+
"attributes": { "slot": "icon" },
|
|
70
|
+
"$props": { "name": "plus" }
|
|
71
|
+
},
|
|
72
|
+
{ "tagName": "span", "textContent": "With icon" }
|
|
73
|
+
]
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"tagName": "jx-button",
|
|
77
|
+
"$props": { "variant": { "$ref": "$map/item" }, "quiet": true },
|
|
78
|
+
"children": [{ "tagName": "span", "textContent": "Quiet" }]
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"tagName": "jx-button",
|
|
82
|
+
"$props": { "variant": { "$ref": "$map/item" }, "disabled": true },
|
|
83
|
+
"children": [{ "tagName": "span", "textContent": "Disabled" }]
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"tagName": "jx-button",
|
|
87
|
+
"$props": { "variant": { "$ref": "$map/item" }, "loading": true },
|
|
88
|
+
"children": [{ "tagName": "span", "textContent": "Loading" }]
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"tagName": "jx-button",
|
|
92
|
+
"$props": { "variant": { "$ref": "$map/item" }, "size": "sm" },
|
|
93
|
+
"children": [{ "tagName": "span", "textContent": "Small" }]
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"tagName": "jx-button",
|
|
97
|
+
"$props": { "variant": { "$ref": "$map/item" }, "size": "lg" },
|
|
98
|
+
"children": [{ "tagName": "span", "textContent": "Large" }]
|
|
99
|
+
}
|
|
100
|
+
]
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
]
|
|
104
|
+
}
|