diff --git a/packages/phoenix-event-display/src/managers/ui-manager/color-options.ts b/packages/phoenix-event-display/src/managers/ui-manager/color-options.ts index bd2b05ff9..6d6d2692e 100644 --- a/packages/phoenix-event-display/src/managers/ui-manager/color-options.ts +++ b/packages/phoenix-event-display/src/managers/ui-manager/color-options.ts @@ -88,7 +88,11 @@ export class ColorOptions { colorByOptionsToInclude?: ColorByOptionKeys[], ) { this.collectionName = collectionFolder.name; - this.colorOptionsFolder = collectionFolder.addChild('Color Options'); + this.colorOptionsFolder = collectionFolder.addChild( + 'Color Options', + undefined, + 'color-options', + ); this.colorOptionsFolder.addConfig({ type: 'color', diff --git a/packages/phoenix-event-display/src/managers/ui-manager/phoenix-menu/phoenix-menu-ui.ts b/packages/phoenix-event-display/src/managers/ui-manager/phoenix-menu/phoenix-menu-ui.ts index f392020cf..adf80bf66 100644 --- a/packages/phoenix-event-display/src/managers/ui-manager/phoenix-menu/phoenix-menu-ui.ts +++ b/packages/phoenix-event-display/src/managers/ui-manager/phoenix-menu/phoenix-menu-ui.ts @@ -298,7 +298,11 @@ export class PhoenixMenuUI implements PhoenixUI { collectionName: string, cuts: Cut[], ) { - const cutsOptionsNode = collectionNode.addChild('Cut Options'); + const cutsOptionsNode = collectionNode.addChild( + 'Cut Options', + undefined, + 'cut-options', + ); cutsOptionsNode .addConfig({ @@ -340,7 +344,11 @@ export class PhoenixMenuUI implements PhoenixUI { collectionNode: PhoenixMenuNode, collectionName: string, ) { - const drawOptionsNode = collectionNode.addChild('Draw Options'); + const drawOptionsNode = collectionNode.addChild( + 'Draw Options', + undefined, + 'draw-options', + ); drawOptionsNode.addConfig({ type: 'slider', diff --git a/packages/phoenix-ng/projects/phoenix-app/src/assets/icons/color-options.svg b/packages/phoenix-ng/projects/phoenix-app/src/assets/icons/color-options.svg new file mode 100644 index 000000000..fb040d2fb --- /dev/null +++ b/packages/phoenix-ng/projects/phoenix-app/src/assets/icons/color-options.svg @@ -0,0 +1,5 @@ + diff --git a/packages/phoenix-ng/projects/phoenix-app/src/assets/icons/cut-options.svg b/packages/phoenix-ng/projects/phoenix-app/src/assets/icons/cut-options.svg new file mode 100644 index 000000000..6585cb10e --- /dev/null +++ b/packages/phoenix-ng/projects/phoenix-app/src/assets/icons/cut-options.svg @@ -0,0 +1,5 @@ + diff --git a/packages/phoenix-ng/projects/phoenix-app/src/assets/icons/draw-options.svg b/packages/phoenix-ng/projects/phoenix-app/src/assets/icons/draw-options.svg new file mode 100644 index 000000000..b3c323301 --- /dev/null +++ b/packages/phoenix-ng/projects/phoenix-app/src/assets/icons/draw-options.svg @@ -0,0 +1,5 @@ + diff --git a/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/phoenix-menu/phoenix-menu-item/phoenix-menu-item.component.html b/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/phoenix-menu/phoenix-menu-item/phoenix-menu-item.component.html index 4c7196da1..66f87d8f8 100644 --- a/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/phoenix-menu/phoenix-menu-item/phoenix-menu-item.component.html +++ b/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/phoenix-menu/phoenix-menu-item/phoenix-menu-item.component.html @@ -1,191 +1,236 @@ -
-
- - - - - - - - - - + +
+ + +
+ + +
+
+ + + + + + + + + + + + + + + {{ + currentNode.name + }} + + +
+
+
+
-
-
+ + +
+
+
+ + +
+
+ + +
+ + +
+ + + +
+
+ +
-
- - -
- -
- + {{ config.label }} + +
+ - - + + - + - + -
-
-
- - -
-
- - -
-
+
+
+
+ + +
+
+ +
- -
- - + + +
-
- + +
-
-
- - -
+ diff --git a/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/phoenix-menu/phoenix-menu-item/phoenix-menu-item.component.scss b/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/phoenix-menu/phoenix-menu-item/phoenix-menu-item.component.scss index b8911373f..d06623cc7 100644 --- a/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/phoenix-menu/phoenix-menu-item/phoenix-menu-item.component.scss +++ b/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/phoenix-menu/phoenix-menu-item/phoenix-menu-item.component.scss @@ -42,58 +42,130 @@ .item-settings { margin-right: 0.2rem; } + } - .item-config-backdrop { - position: fixed; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - z-index: 110; - } - - .item-config { - position: absolute; - right: 100%; - width: 14rem; - padding: 0.5rem; - background: var(--phoenix-background-color-secondary); - color: var(--phoenix-text-color-secondary); - box-shadow: var(--phoenix-box-shadow); - display: flex; - flex-direction: column; - z-index: 120; - max-height: 60vh; - overflow-y: auto; - - /* Custom thin scrollbar to avoid clutter */ - &::-webkit-scrollbar { - width: 4px; - } - &::-webkit-scrollbar-thumb { - background: var(--phoenix-text-color-secondary, #999); - border-radius: 4px; + @media screen and (max-width: 768px) { + font-size: 0.75rem; + padding: 0.5rem; + } +} + +// Compact icon-button variant for leaf nodes holding only configs +// (the collections' Draw/Cut/Color Options). +.phoenix-menu-item-compact { + flex: 1; + min-width: 0; + color: var(--phoenix-text-color); + + .compact-option-button { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.15rem; + padding: 0.3rem 0.2rem; + color: inherit; + border-radius: 0.3rem; + transition: all 0.4s; + + &:hover { + background: var(--phoenix-options-icon-bg); + cursor: pointer; + } + + .compact-option-label { + font-size: 0.65rem; + line-height: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; + } + } +} + +.phoenix-menu-options-bar { + display: flex; + align-items: stretch; + gap: 0.25rem; + padding: 0.25rem 0.4rem; + background: var(--phoenix-background-color-secondary); + + app-phoenix-menu-item { + display: flex; + flex: 1; + min-width: 0; + } +} + +// Rules shared by the full row and the compact variant (the config popup is +// rendered inside both). +.phoenix-menu-item, +.phoenix-menu-item-compact { + .item-config-backdrop { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + z-index: 110; + } + + .item-config { + position: absolute; + right: 100%; + width: 14rem; + padding: 0.5rem; + background: var(--phoenix-background-color-secondary); + color: var(--phoenix-text-color-secondary); + box-shadow: var(--phoenix-box-shadow); + display: flex; + flex-direction: column; + z-index: 120; + max-height: 60vh; + overflow-y: auto; + font-size: 0.8rem; + + /* Custom thin scrollbar to avoid clutter */ + &::-webkit-scrollbar { + width: 4px; + } + &::-webkit-scrollbar-thumb { + background: var(--phoenix-text-color-secondary, #999); + border-radius: 4px; + } + + & > * { + margin-bottom: 0.4rem; + + &:last-child { + margin-bottom: 0; } + } - & > * { - margin-bottom: 0.4rem; + .item-config-single { + display: flex; + flex-direction: row; - &:last-child { - margin-bottom: 0; - } + .item-config-label { + width: 30%; } - .item-config-single { - display: flex; - flex-direction: row; + .item-config-data { + flex-grow: 1; + padding-left: 0.5rem; + min-width: 0; + } - .item-config-label { - width: 30%; - } + // Cut rows have short symbol labels; give the space to the inputs. + &.range-slider-row { + align-items: center; - .item-config-data { - flex-grow: 1; - padding-left: 0.5rem; + .item-config-label { + width: auto; + min-width: 1.6rem; + flex-shrink: 0; + margin-bottom: 0; } } } @@ -146,9 +218,6 @@ } @media screen and (max-width: 768px) { - font-size: 0.75rem; - padding: 0.5rem; - .icon-wrapper { width: 1rem; height: 1rem; @@ -187,13 +256,25 @@ mat-checkbox { flex-shrink: 0; + // Shrink the Material checkbox touch target so the inputs get the space. + --mdc-checkbox-state-layer-size: 24px; } input[type='number'] { min-width: 0; width: 100%; - padding: 0.1rem 0.2rem; - font-size: 0.7rem; + padding: 0.15rem 0.25rem; + font-size: 0.75rem; + + // Hide the native spinner buttons which clip the numbers. + appearance: textfield; + -moz-appearance: textfield; + + &::-webkit-outer-spin-button, + &::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; + } } } } diff --git a/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/phoenix-menu/phoenix-menu-item/phoenix-menu-item.component.test.ts b/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/phoenix-menu/phoenix-menu-item/phoenix-menu-item.component.test.ts index 63bab3e56..2cb0fc590 100644 --- a/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/phoenix-menu/phoenix-menu-item/phoenix-menu-item.component.test.ts +++ b/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/phoenix-menu/phoenix-menu-item/phoenix-menu-item.component.test.ts @@ -37,4 +37,55 @@ describe('PhoenixMenuItemComponent', () => { [], ); }); + + it('should detect compact options nodes', () => { + const optionsNode = component.currentNode.addChild( + 'Color Options', + undefined, + 'color-options', + ); + optionsNode.addConfig({ + type: 'button', + label: 'Random', + onClick: () => {}, + }); + + // A leaf with only configs and an icon is compact. + expect(component.isCompactOptionsNode(optionsNode)).toBe(true); + + // Nodes with a toggle, children, no configs or no icon are not. + const toggleNode = component.currentNode.addChild( + 'Collection', + () => {}, + 'color-options', + ); + toggleNode.addConfig({ + type: 'button', + label: 'Random', + onClick: () => {}, + }); + expect(component.isCompactOptionsNode(toggleNode)).toBe(false); + expect(component.isCompactOptionsNode(component.currentNode)).toBe(false); + }); + + it('should partition children into compact and regular ones', () => { + const optionsNode = component.currentNode.addChild( + 'Draw Options', + undefined, + 'draw-options', + ); + optionsNode.addConfig({ type: 'button', label: 'Test', onClick: () => {} }); + const regularNode = component.currentNode.addChild('Collection', () => {}); + + expect(component.compactChildren).toEqual([optionsNode]); + expect(component.regularChildren).toEqual([regularNode]); + }); + + it('should shorten the compact label', () => { + component.currentNode.name = 'Draw Options'; + expect(component.compactLabel).toBe('Draw'); + + component.currentNode.name = 'Something Else'; + expect(component.compactLabel).toBe('Something Else'); + }); }); diff --git a/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/phoenix-menu/phoenix-menu-item/phoenix-menu-item.component.ts b/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/phoenix-menu/phoenix-menu-item/phoenix-menu-item.component.ts index d96de7fdc..060e1db5d 100644 --- a/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/phoenix-menu/phoenix-menu-item/phoenix-menu-item.component.ts +++ b/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/phoenix-menu/phoenix-menu-item/phoenix-menu-item.component.ts @@ -22,6 +22,41 @@ export class PhoenixMenuItemComponent { constructor(private cdr: ChangeDetectorRef) {} + /** + * Whether a node is a leaf holding only configs (like the collections' + * "Draw/Cut/Color Options"), which is displayed as a compact icon button + * in a toolbar instead of a full menu row. + */ + isCompactOptionsNode(node: PhoenixMenuNode): boolean { + return ( + !!node.icon && + !node.onToggle && + node.children.length === 0 && + node.configs.length > 0 + ); + } + + get isCompact(): boolean { + return this.isCompactOptionsNode(this.currentNode); + } + + /** Label for the compact icon button, e.g. "Draw Options" -> "Draw". */ + get compactLabel(): string { + return this.currentNode.name.replace(/\s+Options$/, ''); + } + + get compactChildren(): PhoenixMenuNode[] { + return this.currentNode.children.filter((child) => + this.isCompactOptionsNode(child), + ); + } + + get regularChildren(): PhoenixMenuNode[] { + return this.currentNode.children.filter( + (child) => !this.isCompactOptionsNode(child), + ); + } + calculateConfigTop() { if (this.phoenixMenuItem) { const itemRect =