From 725c5bdfccce7aa74650603d3fdc06ac471c718e Mon Sep 17 00:00:00 2001 From: Thibault Zanini Date: Wed, 23 Jul 2025 14:15:00 +0200 Subject: [PATCH 01/21] feat: migrate image to ES6 --- ui/image.mod/image.js | 82 +++++++++++++++++++++---------------------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/ui/image.mod/image.js b/ui/image.mod/image.js index e11258e6b..0b3d4bece 100644 --- a/ui/image.mod/image.js +++ b/ui/image.mod/image.js @@ -1,48 +1,48 @@ /** - @module "mod/ui/native/image.mod" - @requires mod/ui/component - @requires mod/ui/native-control -*/ -var Component = require("ui/component").Component; + * @module "mod/ui/native/image.mod" + * @requires mod/ui/component + * @requires mod/ui/native-control + */ +const { Component } = require("ui/component"); /** * Wraps the a <img> element with binding support for its standard attributes. - @class module:"mod/ui/native/image.mod".Image - @extends module:mod/ui/control.Control + * @class module:"mod/ui/native/image.mod".Image + * @extends module:mod/ui/control.Control */ -exports.Image = Component.specialize({ - hasTemplate: {value: true } +const Image = class Image extends Component { + hasTemplate = true; +}; + +/** @lends module:"mod/ui/native/image.mod".Image */ +Image.addAttributes({ + /** + * A text description to display in place of the image. + * @type {string} + * @default null + */ + alt: null, + + /** + * The height of the image in CSS pixels. + * @type {number} + * @default null + */ + height: null, + + /** + * The URL where the image is located. + * @type {string} + * @default null + */ + src: null, + + /** + * The width of the image in CSS pixels. + * @type {number} + * @default null + */ + width: null, }); -exports.Image.addAttributes(/** @lends module:"mod/ui/native/image.mod".Image */{ - -/** - A text description to display in place of the image. - @type {string} - @default null -*/ - alt: null, - -/** - The height of the image in CSS pixels. - @type {number} - @default null -*/ - height: null, - -/** - The URL where the image is located. - @type {string} - @default null -*/ - src: null, - -/** - The width of the image in CSS pixels. - @type {number} - @default null -*/ - width: null - - -}); +exports.Image = Image; From b1a575160d1d09d09fa0a19defa29c614542c749 Mon Sep 17 00:00:00 2001 From: Thibault Zanini Date: Wed, 23 Jul 2025 14:15:21 +0200 Subject: [PATCH 02/21] feat: Added teach for Image component --- index.html | 1 + ui/image.mod/teach/assets/banana.svg | 1 + ui/image.mod/teach/index.html | 20 ++++++++++ ui/image.mod/teach/package.json | 10 +++++ ui/image.mod/teach/ui/main.mod/main.css | 51 ++++++++++++++++++++++++ ui/image.mod/teach/ui/main.mod/main.html | 38 ++++++++++++++++++ ui/image.mod/teach/ui/main.mod/main.js | 5 +++ 7 files changed, 126 insertions(+) create mode 100644 ui/image.mod/teach/assets/banana.svg create mode 100644 ui/image.mod/teach/index.html create mode 100644 ui/image.mod/teach/package.json create mode 100644 ui/image.mod/teach/ui/main.mod/main.css create mode 100644 ui/image.mod/teach/ui/main.mod/main.html create mode 100644 ui/image.mod/teach/ui/main.mod/main.js diff --git a/index.html b/index.html index b26159d51..951090dea 100644 --- a/index.html +++ b/index.html @@ -52,6 +52,7 @@

Components:

Toggle TreeList VirtualList + Image

Managers:

Drag & Drop diff --git a/ui/image.mod/teach/assets/banana.svg b/ui/image.mod/teach/assets/banana.svg new file mode 100644 index 000000000..8b4bd2c8f --- /dev/null +++ b/ui/image.mod/teach/assets/banana.svg @@ -0,0 +1 @@ + diff --git a/ui/image.mod/teach/index.html b/ui/image.mod/teach/index.html new file mode 100644 index 000000000..ad0d6c62b --- /dev/null +++ b/ui/image.mod/teach/index.html @@ -0,0 +1,20 @@ + + + + + + Teach SegmentControl Mod + + + + + + + + diff --git a/ui/image.mod/teach/package.json b/ui/image.mod/teach/package.json new file mode 100644 index 000000000..98815d77d --- /dev/null +++ b/ui/image.mod/teach/package.json @@ -0,0 +1,10 @@ +{ + "name": "teach-segmented-bar-mod", + "private": true, + "dependencies": { + "mod": "*" + }, + "mappings": { + "mod": "../../../" + } +} diff --git a/ui/image.mod/teach/ui/main.mod/main.css b/ui/image.mod/teach/ui/main.mod/main.css new file mode 100644 index 000000000..b382bbae7 --- /dev/null +++ b/ui/image.mod/teach/ui/main.mod/main.css @@ -0,0 +1,51 @@ +body { + font-family: -apple-system, Roboto, sans-serif; + background-color: #fafafa; + max-width: 800px; + margin: 0 auto; + padding: 20px; + + .card { + margin: 40px 0; + padding: 16px; + background: white; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + display: flex; + flex-direction: column; + gap: 24px; + + h2 { + margin: 0; + color: #333; + } + + label { + min-width: 128px; + font-weight: 500; + } + } + + .row { + display: flex; + align-items: center; + gap: 24px; + } + + .column { + display: flex; + flex-direction: column; + gap: 24px; + align-items: flex-start; + + &.stretch { + align-items: stretch; + } + } + + .grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 16px; + } +} diff --git a/ui/image.mod/teach/ui/main.mod/main.html b/ui/image.mod/teach/ui/main.mod/main.html new file mode 100644 index 000000000..b1bd25d11 --- /dev/null +++ b/ui/image.mod/teach/ui/main.mod/main.html @@ -0,0 +1,38 @@ + + + + + Teach Image Mod + + + + +
+

Image

+ + +
+

Basic Usage

+
+ +
+
+
+
+ + diff --git a/ui/image.mod/teach/ui/main.mod/main.js b/ui/image.mod/teach/ui/main.mod/main.js new file mode 100644 index 000000000..15dfb7465 --- /dev/null +++ b/ui/image.mod/teach/ui/main.mod/main.js @@ -0,0 +1,5 @@ +const { Component } = require("mod/ui/component"); + +exports.Main = class Main extends Component { + +}; From 40cdccb009b21c67fefb9dd75f99cd99b473d131 Mon Sep 17 00:00:00 2001 From: Thibault Zanini Date: Thu, 24 Jul 2025 15:09:51 +0200 Subject: [PATCH 03/21] feat: add slotContentDidFirstDraw delegate method Allow delegates to be notified when slot content has been drawn for the first time. This enables delegates to perform initialization logic or side effects that depend on the slot's content being rendered in the DOM. --- ui/slot.mod/slot.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/ui/slot.mod/slot.js b/ui/slot.mod/slot.js index ede32f275..a81e88cb3 100644 --- a/ui/slot.mod/slot.js +++ b/ui/slot.mod/slot.js @@ -41,6 +41,22 @@ exports.Slot = Component.specialize( /** @lends Slot.prototype # */ { if (firstTime) { this.element.classList.add("slot-mod"); } + + this.addEventListener("firstDraw", this, false); + } + }, + + exitDocument: { + value: function () { + this.removeEventListener("firstDraw", this, false); + } + }, + + handleFirstDraw: { + value: function () { + if (this.delegate && typeof this.delegate.slotContentDidFirstDraw === "function") { + this.delegate.slotContentDidFirstDraw(this); + } } }, From d3229f2f5a5d5a54fe061c595332b46dacdd15a9 Mon Sep 17 00:00:00 2001 From: Thibault Zanini Date: Thu, 24 Jul 2025 15:54:13 +0200 Subject: [PATCH 04/21] feat: enhance delegate method handling with caching and new response check --- core/core.js | 146 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 99 insertions(+), 47 deletions(-) diff --git a/core/core.js b/core/core.js index 5b39a8dae..fda152c52 100644 --- a/core/core.js +++ b/core/core.js @@ -1280,9 +1280,9 @@ Montage.defineProperty(Montage.prototype, "version", { * This is an evolution to progressively remove the reliance on the additional * serializable property set on JS PropertyDescritpors, and instead relay on setting in ObjectDescriptors * property descriptors. The range of value is unusual as it is a blend of string and boolean.... - * + * * Posible values: "reference" | "value" | "auto" | false, - * + * * @type {string | boolean} . */ @@ -1303,7 +1303,7 @@ Montage.defineProperty(Montage.prototype, "_buildSerializablePropertyNames", { let _serializablePropertyNames, legacySerializablePropertyNames = Montage.getSerializablePropertyNames(this); - + Montage.defineProperty(Object.getPrototypeOf(this), "_serializablePropertyNames", { value: (_serializablePropertyNames = this.objectDescriptor ? this.objectDescriptor.serializablePropertyDescriptors.map((aPropertyDescriptor) => { @@ -1356,60 +1356,112 @@ Montage.defineProperty(Montage, "equals", { }); /** - * This method calls the method named with the identifier prefix if it exists. - * Example: If the name parameter is "shouldDoSomething" and the caller's identifier is "bob", then - * this method will try and call "bobShouldDoSomething" + * Calls the delegate method with the specified name if it exists on the delegate object. + * Uses caching to avoid repeated method lookups since delegate methods are unlikely to be removed dynamically. * - * TODO: Cache!!!! We're unlikely to remove a delegate method dynamically, so we should avoid checking all - * that and just cache the function found, using a weak map, so don't retain delegates. + * The method first attempts to find a method with the pattern `{identifier}{Name}` on the delegate, + * where the first letter of the name parameter is capitalized. If not found, it falls back to + * looking for a method with the exact name. * * @function Montage#callDelegateMethod - * @param {string} name -*/ + * @param {string} name - The name of the delegate method to call + * @param {...*} args - Additional arguments to pass to the delegate method + * @returns {*} The return value of the delegate method, or undefined if no method was found or no delegate exists + * + * @example + * // If this.identifier is "bob" and name is "shouldDoSomething" + * // This will try to call "bobShouldDoSomething" on the delegate + * const result = this.callDelegateMethod("shouldDoSomething", arg1, arg2); + */ Montage.defineProperty(Montage.prototype, "callDelegateMethod", { value: function (name) { - var delegate = this.delegate, delegateFunction; + const delegateFunction = this.getDelegateMethod(name); - if (delegate) { + if (this.delegate && delegateFunction) { + const [, ...rest] = arguments; + return delegateFunction.call(this.delegate, ...rest); + } + } +}); - var delegateFunctionName = this.identifier; - delegateFunctionName += name.toCapitalized(); +/** + * Checks whether the delegate object has a method that responds to the specified name. + * This method uses the same lookup logic as callDelegateMethod and getDelegateMethod. + * + * @function Montage#respondsToDelegateMethod + * @param {string} name - The name of the delegate method to check for + * @returns {boolean} True if the delegate has a method that responds to the given name, false otherwise + * + * @example + * // Check if delegate can handle "shouldDoSomething" + * if (this.respondsToDelegateMethod("shouldDoSomething")) { + * this.callDelegateMethod("shouldDoSomething", data); + * } + */ +Montage.defineProperty(Montage.prototype, "respondsToDelegateMethod", { + value: function (name) { + return typeof this.getDelegateMethod(name) === FUNCTION; + } +}); - if ( - typeof this.identifier === "string" && - typeof delegate[delegateFunctionName] === FUNCTION - ) { - delegateFunction = delegate[delegateFunctionName]; - } else if (typeof delegate[name] === FUNCTION) { - delegateFunction = delegate[name]; - } +// WeakMap to cache delegate methods - won't retain delegates when they're garbage collected +const delegateMethodCache = new WeakMap(); - if (delegateFunction) { - //Using modern JS: - // Destructure the array to skip the first element - const [, ...rest] = arguments; - return delegateFunction.call(delegate, ...rest); - - // if(arguments.length === 2) { - // return delegateFunction.call(delegate,arguments[1]); - // } - // else if(arguments.length === 3) { - // return delegateFunction.call(delegate,arguments[1],arguments[2]); - // } - // else if(arguments.length === 4) { - // return delegateFunction.call(delegate,arguments[1],arguments[2],arguments[3]); - // } - // else if(arguments.length === 5) { - // return delegateFunction.call(delegate,arguments[1],arguments[2],arguments[3],arguments[4]); - // } - // else { - // // remove first argument - // ARRAY_PROTOTYPE.shift.call(arguments); - // return delegateFunction.apply(delegate, arguments); - // } - } +/** + * Retrieves a delegate method by name, using caching for performance optimization. + * + * The method searches for delegate methods in the following order: + * 1. First tries `{identifier}{Name}` where Name is the capitalized version of the name parameter + * 2. Falls back to the exact method name if the prefixed version doesn't exist + * + * Results are cached using a WeakMap to avoid repeated lookups while ensuring + * delegates can still be garbage collected when no longer referenced. + * + * @function Montage#getDelegateMethod + * @param {string} name - The name of the delegate method to retrieve + * @returns {Function|undefined} The delegate method function if found, undefined otherwise + * + * @example + * // If this.identifier is "list" and name is "shouldSelectItem" + * // This will look for "listShouldSelectItem" first, then "shouldSelectItem" + * const method = this.getDelegateMethod("shouldSelectItem"); + * if (method) { + * method.call(this.delegate, item); + * } + */ +Montage.defineProperty(Montage.prototype, "getDelegateMethod", { + value: function (name) { + if (!this.delegate) return; + + const delegate = this.delegate; + let delegateCache = delegateMethodCache.get(delegate); + + if (!delegateCache) { + delegateCache = new Map(); + delegateMethodCache.set(delegate, delegateCache); } - } + + // Check if we already have the function cached + if (delegateCache.has(name)) return delegateCache.get(name); + + let delegateFunctionName = this.identifier; + let delegateFunction; + + delegateFunctionName += name.toCapitalized(); + + if (typeof this.identifier === "string" && typeof delegate[delegateFunctionName] === FUNCTION) { + delegateFunction = delegate[delegateFunctionName]; + } else if (typeof delegate[name] === FUNCTION) { + delegateFunction = delegate[name]; + } + + // Cache the delegate function if it exists + if (delegateFunction) { + delegateCache.set(name, delegateFunction); + } + + return delegateFunction; + }, }); // Property Changes From 772c60b1eb9d8def8cc0c561ae67adebbcbe264f Mon Sep 17 00:00:00 2001 From: Thibault Zanini Date: Thu, 24 Jul 2025 15:55:06 +0200 Subject: [PATCH 05/21] refactor: migrate Slot class to ES6 and improve delegate method handling --- ui/slot.mod/slot.js | 130 ++++++++++++++++++-------------------------- 1 file changed, 54 insertions(+), 76 deletions(-) diff --git a/ui/slot.mod/slot.js b/ui/slot.mod/slot.js index a81e88cb3..edecf132b 100644 --- a/ui/slot.mod/slot.js +++ b/ui/slot.mod/slot.js @@ -1,8 +1,8 @@ /** - @module "mod/ui/slot.mod" - @requires mod/ui/component -*/ -var Component = require("../component").Component; + * @module "mod/ui/slot.mod" + * @requires mod/ui/component + */ +const { Component } = require("../component"); /** * @class Slot @@ -10,13 +10,7 @@ var Component = require("../component").Component; * other component. * @extends Component */ -exports.Slot = Component.specialize( /** @lends Slot.prototype # */ { - - hasTemplate: { - enumerable: false, - value: false - }, - +exports.Slot = class Slot extends Component { /** * An optional helper object. The slot consults * `delegate.slotElementForComponent(component):Element` if available for @@ -28,90 +22,74 @@ exports.Slot = Component.specialize( /** @lends Slot.prototype # */ { * @type {?Object} * @default null */ - delegate: { - value: null - }, - - _content: { - value: null - }, + delegate = null; - enterDocument:{ - value:function (firstTime) { - if (firstTime) { - this.element.classList.add("slot-mod"); - } - - this.addEventListener("firstDraw", this, false); - } - }, + _content = null; - exitDocument: { - value: function () { - this.removeEventListener("firstDraw", this, false); - } - }, - - handleFirstDraw: { - value: function () { - if (this.delegate && typeof this.delegate.slotContentDidFirstDraw === "function") { - this.delegate.slotContentDidFirstDraw(this); - } - } - }, + get hasTemplate() { + return false; + } /** * The component that resides in this slot and in its place in the * template. * @type {Component} * @default null - */ - content: { - get: function () { - return this._content; - }, - set: function (value) { - var element, - content; + */ + get content() { + return this._content; + } - if (value && typeof value.needsDraw !== "undefined") { - content = this._content; + set content(value) { + let element; - // If the incoming content was a component; make sure it has an element before we say it needs to draw - if (!value.element) { - element = document.createElement("div"); + if (value && typeof value.needsDraw !== "undefined") { + // If the incoming content was a component; + // make sure it has an element before we say it needs to draw + if (!value.element) { + element = document.createElement("div"); - if (this.delegate && typeof this.delegate.slotElementForComponent === "function") { - element = this.delegate.slotElementForComponent(this, value, element); - } - value.element = element; - } else { - element = value.element; + if (this.respondsToDelegateMethod("slotElementForComponent")) { + element = this.callDelegateMethod("slotElementForComponent", this, value, element); } - - // The child component will need to draw; this may trigger a draw for the slot itself - this.domContent = element; - value.needsDraw = true; - + value.element = element; } else { - this.domContent = value; + element = value.element; } - this._content = value; - this.needsDraw = true; + // The child component will need to draw; + // this may trigger a draw for the slot itself + this.domContent = element; + value.needsDraw = true; + } else { + this.domContent = value; } - }, + + this._content = value; + this.needsDraw = true; + } + + enterDocument(firstTime) { + if (firstTime) { + this.element.classList.add("slot-mod"); + } + + this.addEventListener("firstDraw", this, false); + } + + exitDocument() { + this.removeEventListener("firstDraw", this, false); + } + + handleFirstDraw() { + this.callDelegateMethod("slotContentDidFirstDraw", this); + } /** * Informs the `delegate` that `slotDidSwitchContent(slot)` * @function */ - contentDidChange: { - value: function () { - if (this.delegate && typeof this.delegate.slotDidSwitchContent === "function") { - this.delegate.slotDidSwitchContent(this); - } - } + contentDidChange() { + this.callDelegateMethod("slotDidSwitchContent", this); } - -}); +}; From 7e2ed6de2847dfc4829caa6634f1a2d4276b11a8 Mon Sep 17 00:00:00 2001 From: Thibault Zanini Date: Fri, 25 Jul 2025 14:39:13 +0200 Subject: [PATCH 06/21] feat: add loading attribute to Image component --- ui/image.mod/image.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/ui/image.mod/image.js b/ui/image.mod/image.js index 0b3d4bece..010e25d56 100644 --- a/ui/image.mod/image.js +++ b/ui/image.mod/image.js @@ -43,6 +43,17 @@ Image.addAttributes({ * @default null */ width: null, + + /** + * The loading strategy for the image. + * @type {string} + * @default "eager" + * @values ["eager", "lazy"] + */ + loading: { + dataType: "string", + value: "eager", + }, }); exports.Image = Image; From 5de0483d0c47c8701182407da6e5d3c9223e2160 Mon Sep 17 00:00:00 2001 From: Thibault Zanini Date: Fri, 18 Jul 2025 17:06:33 +0200 Subject: [PATCH 07/21] feat: added VisualShape enum --- core/enums/visual-shape.js | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 core/enums/visual-shape.js diff --git a/core/enums/visual-shape.js b/core/enums/visual-shape.js new file mode 100644 index 000000000..72c791caf --- /dev/null +++ b/core/enums/visual-shape.js @@ -0,0 +1,9 @@ +const { Enum } = require("../enum"); + +/** + * @typedef {"rectangle"|"rounded"|'pill'} VisualShape + */ +exports.VisualShape= new Enum().initWithMembersAndValues( + ["rectangle", "rounded", "pill"], + ["mod--shape-rectangle", "mod--shape-rounded", "mod--shape-pill"] +); From 9047f757ce8150420432d6012f78dbb71a0d0c2d Mon Sep 17 00:00:00 2001 From: Thibault Zanini Date: Fri, 18 Jul 2025 17:06:46 +0200 Subject: [PATCH 08/21] feat: added VisualSize enum --- core/enums/visual-size.js | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 core/enums/visual-size.js diff --git a/core/enums/visual-size.js b/core/enums/visual-size.js new file mode 100644 index 000000000..684155e9d --- /dev/null +++ b/core/enums/visual-size.js @@ -0,0 +1,9 @@ +const { Enum } = require("../enum"); + +/** + * @typedef {"small"|"medium"|'large'} VisualSize + */ +exports.VisualSize= new Enum().initWithMembersAndValues( + ["small", "medium", "large"], + ["mod--size-small", "mod--size-medium", "mod--size-large"] +); From 5d85b8909a6931480b71070efa6591bb40d00a24 Mon Sep 17 00:00:00 2001 From: Thibault Zanini Date: Fri, 18 Jul 2025 17:07:33 +0200 Subject: [PATCH 09/21] feat: added values getter on enum --- core/enum.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/enum.js b/core/enum.js index 56752f439..44a5519fc 100644 --- a/core/enum.js +++ b/core/enum.js @@ -219,6 +219,12 @@ exports.Enum = Montage.specialize( /** @lends Enum# */ { } } } + }, + + values: { + get: function () { + return this._members.map( (member) => this[member]); + } } }); From 138b47bab5febddbcc90a3019fe5b356a3740eb2 Mon Sep 17 00:00:00 2001 From: Thibault Zanini Date: Fri, 18 Jul 2025 17:17:52 +0200 Subject: [PATCH 10/21] chore: add prettierrc config --- .prettierrc | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .prettierrc diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 000000000..08d6d29d7 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "tabWidth": 4, + "printWidth": 120 +} From 423b0d621710c13ca2b0d98fce923a019ee10191 Mon Sep 17 00:00:00 2001 From: Thibault Zanini Date: Fri, 18 Jul 2025 17:34:39 +0200 Subject: [PATCH 11/21] feat: added SegmentedControl component # Conflicts: # index.html --- index.html | 1 + .../segment.mod/segment.css | 106 ++++++ .../segment.mod/segment.html | 45 +++ .../segment.mod/segment.js | 137 ++++++++ .../segmented-control.css | 212 ++++++++++++ .../segmented-control.html | 50 +++ ui/segmented-control.mod/segmented-control.js | 311 ++++++++++++++++++ .../teach/assets/apple.svg | 1 + .../teach/assets/banana.svg | 1 + .../teach/assets/grappe.svg | 1 + ui/segmented-control.mod/teach/index.html | 20 ++ ui/segmented-control.mod/teach/package.json | 10 + .../teach/ui/main.mod/main.css | 71 ++++ .../teach/ui/main.mod/main.html | 306 +++++++++++++++++ .../teach/ui/main.mod/main.js | 16 + 15 files changed, 1288 insertions(+) create mode 100644 ui/segmented-control.mod/segment.mod/segment.css create mode 100644 ui/segmented-control.mod/segment.mod/segment.html create mode 100644 ui/segmented-control.mod/segment.mod/segment.js create mode 100644 ui/segmented-control.mod/segmented-control.css create mode 100644 ui/segmented-control.mod/segmented-control.html create mode 100644 ui/segmented-control.mod/segmented-control.js create mode 100644 ui/segmented-control.mod/teach/assets/apple.svg create mode 100644 ui/segmented-control.mod/teach/assets/banana.svg create mode 100644 ui/segmented-control.mod/teach/assets/grappe.svg create mode 100644 ui/segmented-control.mod/teach/index.html create mode 100644 ui/segmented-control.mod/teach/package.json create mode 100644 ui/segmented-control.mod/teach/ui/main.mod/main.css create mode 100644 ui/segmented-control.mod/teach/ui/main.mod/main.html create mode 100644 ui/segmented-control.mod/teach/ui/main.mod/main.js diff --git a/index.html b/index.html index 951090dea..1abeecd0a 100644 --- a/index.html +++ b/index.html @@ -53,6 +53,7 @@

Components:

TreeList VirtualList Image + Segmented bar

Managers:

Drag & Drop diff --git a/ui/segmented-control.mod/segment.mod/segment.css b/ui/segmented-control.mod/segment.mod/segment.css new file mode 100644 index 000000000..82cdddf8e --- /dev/null +++ b/ui/segmented-control.mod/segment.mod/segment.css @@ -0,0 +1,106 @@ +:root { + /* Segment */ + --mod-segment-text: rgba(0, 0, 0, 0.65); + --mod-segment-transition: color 0.25s ease-out, background-color 0.25s ease-out; + + /* Segment heights */ + --mod-segment-height-small: 32px; + --mod-segment-height-medium: 36px; + --mod-segment-height-large: 44px; + + /* Segment Hover State */ + --mod-segment-hover-background: rgba(0, 0, 0, 0.02); + --mod-segment-hover-text: rgba(0, 0, 0, 1); + + /* Segment Active State */ + --mod-segment-active-background: rgba(0, 0, 0, 0.04); + --mod-segment-active-text: rgba(0, 0, 0, 1); + + /* Segment Border Sizes */ + --mod-segment-border-radius-small: 6px; + --mod-segment-border-radius-medium: 8px; + --mod-segment-border-radius-large: 12px; + + /* Segment sizes */ + --mod-segment-padding-small: 4px 8px; + --mod-segment-padding-medium: 8px 12px; + --mod-segment-padding-large: 12px 20px; + --mod-segment-font-size-small: 12px; + --mod-segment-font-size-medium: 13px; + --mod-segment-font-size-large: 16px; +} + +/* Dark Theme Variables */ +@media (prefers-color-scheme: dark) { + :root { + /* Segment - Dark Theme */ + --mod-segment-text: rgba(160, 160, 160, 0.65); + + /* Segment Hover State - Dark Theme */ + --mod-segment-hover-background: rgba(255, 255, 255, 0.08); + --mod-segment-hover-text: rgba(255, 255, 255, 0.9); + + /* Segment Active State - Dark Theme */ + --mod-segment-active-background: rgba(255, 255, 255, 0.12); + --mod-segment-active-text: rgba(255, 255, 255, 1); + } +} + +@scope (.ModSegment) { + :scope { + display: flex; + align-items: center; + justify-content: center; + color: var(--mod-segment-text); + cursor: pointer; + transition: var(--mod-segment-transition); + z-index: 2; + box-sizing: border-box; + white-space: nowrap; + user-select: none; + -webkit-user-select: none; + outline: none; + border: none; + background: transparent; + font-family: inherit; + font-size: inherit; + gap: 8px; + + &:hover:not(.mod--selected):not(.mod--disabled) { + background-color: var(--mod-segment-hover-background); + color: var(--mod-segment-hover-text); + } + + &:active:not(.mod--selected):not(.mod--disabled) { + background-color: var(--mod-segment-active-background); + color: var(--mod-segment-active-text); + } + + &.mod--selected { + color: var(--mod-segmented-control-thumb-text); + } + + &.mod--disabled { + opacity: 0.6; + cursor: not-allowed; + pointer-events: none; + } + + /* TESTING */ + + .ModSegment-leading, + .ModSegment-trailing, + .ModSegment-label { + &:is(:empty) { + display: none; + } + } + + .ModSegment-leading, + .ModSegment-trailing { + display: flex; + align-items: center; + justify-content: center; + } + } +} diff --git a/ui/segmented-control.mod/segment.mod/segment.html b/ui/segmented-control.mod/segment.mod/segment.html new file mode 100644 index 000000000..1f824d9b4 --- /dev/null +++ b/ui/segmented-control.mod/segment.mod/segment.html @@ -0,0 +1,45 @@ + + + + + + + +
+
+
+
+
+ + diff --git a/ui/segmented-control.mod/segment.mod/segment.js b/ui/segmented-control.mod/segment.mod/segment.js new file mode 100644 index 000000000..60176d1a7 --- /dev/null +++ b/ui/segmented-control.mod/segment.mod/segment.js @@ -0,0 +1,137 @@ +const { Component } = require("ui/component"); + +/** + * A segment component representing an individual option in a segmented control. + * @class Segment + * @extends Component + */ +const Segment = class Segment extends Component { + /** + * The label text displayed in the segment + * @type {string} + */ + _label = ""; + + get label() { + return this._label; + } + + set label(value) { + if (this._label === value) return; + this._label = value; + this.needsDraw = true; + } + + /** + * The value associated with this segment + * @type {*} + */ + _value = null; + + get value() { + return this._value; + } + + set value(val) { + if (this._value === val) return; + this._value = val; + } + + /** + * Whether this segment is currently selected + * @type {boolean} + */ + _selected = false; + + get selected() { + return this._selected; + } + + set selected(value) { + if (this._selected === value) return; + this._selected = Boolean(value); + this.needsDraw = true; + } + + /** + * Whether this segment is disabled + * @type {boolean} + */ + _disabled = false; + + get disabled() { + return this._disabled; + } + + set disabled(value) { + if (this._disabled === value) return; + this._disabled = Boolean(value); + this.needsDraw = true; + } + + /** + * The segment option object containing label, value, and disabled state + * @type {Object} + */ + _option = null; + + get option() { + return this._option; + } + + set option(value) { + if (this._option === value) return; + + this._option = value; + + if (value) { + this.label = value.label; + this.value = value.value; + this.disabled = Boolean(value.disabled); + } + } + + draw() { + this._applySelectedClass(); + this._applyDisabledClass(); + this._updateTabIndex(); + } + + /** + * Applies or removes the selected CSS class + * @private + */ + _applySelectedClass() { + if (this._selected) { + this.element.classList.add("mod--selected"); + } else { + this.element.classList.remove("mod--selected"); + } + } + + /** + * Applies or removes the disabled CSS class + * @private + */ + _applyDisabledClass() { + if (this._disabled) { + this.element.classList.add("mod--disabled"); + } else { + this.element.classList.remove("mod--disabled"); + } + } + + /** + * Updates the tabindex based on disabled state + * @private + */ + _updateTabIndex() { + this.element.tabIndex = this._disabled ? -1 : 0; + } +}; + +if (window.MontageElement) { + MontageElement.define("mod-segment", Segment); +} + +exports.Segment = Segment; diff --git a/ui/segmented-control.mod/segmented-control.css b/ui/segmented-control.mod/segmented-control.css new file mode 100644 index 000000000..f6d07d969 --- /dev/null +++ b/ui/segmented-control.mod/segmented-control.css @@ -0,0 +1,212 @@ +:root { + --mod-segmented-control-background: rgba(0, 0, 0, 0.06); + --mod-segmented-control-padding: 2px; + + /* Thumb */ + --mod-segmented-control-thumb-background: rgba(255, 255, 255, 0.85); + --mod-segmented-control-thumb-border-color: rgba(255, 255, 255, 0.975); + --mod-segmented-control-thumb-shadow: 0px 0px 3px 1px rgba(0, 0, 0, 0.04); + --mod-segmented-control-thumb-text: rgba(0, 0, 0, 0.85); + --mod-segmented-control-thumb-transition: all 0.35s cubic-bezier(0.375, 0.075, 0, 1.125); +} + +/* Dark Theme Variables */ +@media (prefers-color-scheme: dark) { + :root { + /* Segmented Control Background */ + --mod-segmented-control-background: rgba(0, 0, 0, 0.8); + + /* Thumb - Dark Theme */ + --mod-segmented-control-thumb-background: rgba(255, 255, 255, 0.08); + --mod-segmented-control-thumb-border-color: rgba(255, 255, 255, 0.04); + --mod-segmented-control-thumb-shadow: 0px 0px 4px 1px rgba(0, 0, 0, 0.3); + --mod-segmented-control-thumb-text: rgba(255, 255, 255, 0.95); + } +} + +@scope (.ModSegmentedControl) { + :scope { + display: inline-block; + -webkit-tap-highlight-color: transparent; + + &.mod--disabled { + opacity: 0.6; + pointer-events: none; + } + + &.mod--ready { + /* This class is added after the first draw */ + /* FIXME: @benoit: maybe a better way of doing that, draw gates maybe ? */ + .ModSegmentedControl-container { + .ModSegmentedControl-thumb { + transition: var(--mod-segmented-control-thumb-transition); + } + } + } + + .ModSegmentedControl-container { + display: flex; + background-color: var(--mod-segmented-control-background); + padding: var(--mod-segmented-control-padding); + position: relative; + + .ModSegmentedControl-thumb { + position: absolute; + top: var(--mod-segmented-control-padding); + left: var(--mod-segmented-control-padding); + background-color: var(--mod-segmented-control-thumb-background); + backdrop-filter: blur(10px); + border: 2px solid var(--mod-segmented-control-thumb-border-color); + box-sizing: border-box; + box-shadow: var(--mod-segmented-control-thumb-shadow); + z-index: 1; + pointer-events: none; + } + + .ModSegmentedControl-segments { + display: flex; + flex-wrap: nowrap; + overflow: hidden; + position: relative; + } + } + } + + /* Orientation */ + + &.mod--horizontal { + .ModSegmentedControl-container { + .ModSegmentedControl-thumb { + height: calc(100% - var(--mod-segmented-control-padding) * 2); + } + + .ModSegmentedControl-segments { + flex-direction: row; + } + } + } + + &.mod--vertical { + .ModSegmentedControl-container { + .ModSegmentedControl-thumb { + width: calc(100% - var(--mod-segmented-control-padding) * 2); + } + + .ModSegmentedControl-segments { + flex-direction: column; + width: 100%; + + .ModSegmentedControl-segment { + width: 100%; + text-align: left; + } + } + } + } + + /* Size */ + + &.mod--size-small { + .ModSegmentedControl-container { + .ModSegmentedControl-segment { + padding: var(--mod-segment-padding-small); + min-height: var(--mod-segment-height-small); + font-size: var(--mod-segment-font-size-small); + } + } + } + + &.mod--size-medium { + .ModSegmentedControl-container { + .ModSegmentedControl-segment { + padding: var(--mod-segment-padding-medium); + min-height: var(--mod-segment-height-medium); + font-size: var(--mod-segment-font-size-medium); + } + } + } + + &.mod--size-large { + .ModSegmentedControl-container { + .ModSegmentedControl-segment { + padding: var(--mod-segment-padding-large); + min-height: var(--mod-segment-height-large); + font-size: var(--mod-segment-font-size-large); + } + } + } + + /* Shapes */ + + &.mod--shape-pill { + &.mod--vertical { + /* TODO: Consider handling this logic in JavaScript for greater resilience and flexibility */ + &.mod--size-small { + .ModSegmentedControl-container { + border-radius: calc(var(--mod-segment-height-small) / 2 + var(--mod-segmented-control-padding)); + } + } + + &.mod--size-medium { + .ModSegmentedControl-container { + border-radius: calc(var(--mod-segment-height-medium) / 2 + var(--mod-segmented-control-padding)); + } + } + + &.mod--size-large { + .ModSegmentedControl-container { + border-radius: calc(var(--mod-segment-height-large) / 2 + var(--mod-segmented-control-padding)); + } + } + } + + &.mod--size-small, + &.mod--size-medium, + &.mod--size-large { + .ModSegmentedControl-container { + /* 50% doesn't work for rectangle shape */ + border-radius: 99999px; + + .ModSegmentedControl-thumb, + .ModSegmentedControl-segment { + border-radius: 99999px; + } + } + } + } + + &.mod--shape-rounded { + &.mod--size-small { + .ModSegmentedControl-container { + border-radius: var(--mod-segment-border-radius-small); + + .ModSegmentedControl-thumb, + .ModSegmentedControl-segment { + border-radius: calc(var(--mod-segment-border-radius-small) - var(--mod-segmented-control-padding)); + } + } + } + + &.mod--size-medium { + .ModSegmentedControl-container { + border-radius: var(--mod-segment-border-radius-medium); + + .ModSegmentedControl-thumb, + .ModSegmentedControl-segment { + border-radius: calc(var(--mod-segment-border-radius-medium) - var(--mod-segmented-control-padding)); + } + } + } + + &.mod--size-large { + .ModSegmentedControl-container { + border-radius: var(--mod-segment-border-radius-large); + + .ModSegmentedControl-thumb, + .ModSegmentedControl-segment { + border-radius: calc(var(--mod-segment-border-radius-large) - var(--mod-segmented-control-padding)); + } + } + } + } +} diff --git a/ui/segmented-control.mod/segmented-control.html b/ui/segmented-control.mod/segmented-control.html new file mode 100644 index 000000000..59da24dd4 --- /dev/null +++ b/ui/segmented-control.mod/segmented-control.html @@ -0,0 +1,50 @@ + + + + + + + +
+
+
+
+
+
+
+
+ + diff --git a/ui/segmented-control.mod/segmented-control.js b/ui/segmented-control.mod/segmented-control.js new file mode 100644 index 000000000..aa1c4902d --- /dev/null +++ b/ui/segmented-control.mod/segmented-control.js @@ -0,0 +1,311 @@ +const { VisualOrientation } = require("core/enums/visual-orientation"); +const { VisualShape } = require("core/enums/visual-shape"); +const { VisualSize } = require("core/enums/visual-size"); +const { Component } = require("ui/component"); + +/** + * A segmented control component that allows users to select from multiple options. + * Displays options as segments with a sliding thumb indicator for the selected option. + * + * @class SegmentedControl + * @extends Component + */ +const SegmentedControl = class SegmentedControl extends Component { + // FIXME: @Benoit workaround: until `removeRangeAtPathChangeListener` is implemented + _cancelHandleOptionsChange = null; + + /** + * The currently selected option value + * @type {*} + */ + selection = null; + + /** + * The disabled state of the segmented control + * @returns {boolean} True if disabled, false otherwise + */ + _disabled = false; + + get disabled() { + return this._disabled; + } + + set disabled(value) { + if (this._disabled === value) return; + + this._disabled = Boolean(value); + this.needsDraw = true; + } + + /** + * The array of options for the segmented control + * @returns {Array} The options array + */ + _options = []; + + get options() { + return this._options; + } + + set options(value) { + if (this._options === value) return; + + if (Array.isArray(value)) { + this._options = value; + } else { + console.warn("Options must be an array."); + this._options = []; + } + + this._normalizedOptions = this._normalizeOptions(); + this.needsDraw = true; + } + + /** + * The orientation of the segmented control + * @returns {string} The current orientation (horizontal or vertical) + */ + _orientation = VisualOrientation.horizontal; + + get orientation() { + return this._orientation; + } + + set orientation(value) { + if (this._orientation === value) return; + + if (!VisualOrientation.members.includes(value)) { + console.warn("Invalid orientation value. Defaulting to horizontal."); + this._orientation = VisualOrientation.horizontal; + } else { + this._orientation = VisualOrientation[value]; + } + + this.needsDraw = true; + } + + /** + * The size of the segmented control + * @returns {string} The current size + */ + _size = VisualSize.medium; + + get size() { + return this._size; + } + + set size(value) { + if (this._size === value) return; + + if (!VisualSize.members.includes(value)) { + console.warn("Invalid size value. Defaulting to medium."); + this._size = VisualSize.medium; + } else { + this._size = VisualSize[value]; + } + + this.needsDraw = true; + } + + /** + * The shape of the segmented control + * @returns {string} The current shape + */ + _shape = VisualShape.rounded; + + get shape() { + return this._shape; + } + + set shape(value) { + if (this._shape === value) return; + + if (!VisualShape.members.includes(value)) { + console.warn("Invalid shape value. Defaulting to 'rounded'."); + this._shape = VisualShape.rounded; + } else { + this._shape = VisualShape[value]; + } + + this.needsDraw = true; + } + + enterDocument() { + this._cancelHandleOptionsChange = this.addRangeAtPathChangeListener("_options", this, "handleOptionsChange"); + this.addPathChangeListener("_selectedOption", this, "handleSelectionChange"); + this._watchForDOMChanges(); + } + + exitDocument() { + this.removePathChangeListener("_selectedOption", this); + this._cancelHandleOptionsChange?.(); + this._mutationObserver.disconnect(); + } + + /** + * Handles changes to the options array. + * Re-normalizes options and triggers a redraw. + */ + handleOptionsChange() { + this._normalizedOptions = this._normalizeOptions(); + this.needsDraw = true; + } + + /** + * Handles selection changes and dispatches a change event + * @param {Object} option - The selected option object + */ + handleSelectionChange(option) { + const event = new CustomEvent("change", { + detail: option, + bubbles: true, + }); + + this.selection = option?.value || null; + this.dispatchEvent(event); + this.needsDraw = true; + } + + draw() { + // Apply classes based on shape, size, and orientation + this._applyOrientationClasses(); + this._applyDisabledClass(); + this._applyShapeClasses(); + this._applySizeClasses(); + + // Move the thumb to the selected segment if available + const { selectedIterations = [] } = this.templateObjects?.segments ?? {}; + const [selectedIteration] = selectedIterations; + + if (selectedIteration) { + this.thumbElement.style.display = "block"; + const segmentElement = selectedIteration.firstElement; + this._moveThumbToSegment(segmentElement); + } else { + // If no segment is selected, hide the thumb + this.thumbElement.style.display = "none"; + } + } + + didDraw() { + if (this._completedFirstDraw) { + this.element.classList.add("mod--ready"); + } + } + + // FIXME: See with @Benoit if we can avoid to render the segments when nested placeholders are not ready + // Draw Gates Maybe? + // Meanwhile, we need to watch for DOM changes + _watchForDOMChanges() { + this._mutationObserver = new MutationObserver((mutations) => { + mutations.some((mutation) => { + if (mutation.type === "childList") { + this.needsDraw = true; + return true; // Stop observing further mutations + } + + return false; // Continue observing + }); + }); + + // Configure what to observe + const config = { + characterDataOldValue: false, + attributeOldValue: false, + characterData: false, + attributes: false, + childList: true, + subtree: true, + }; + + this._mutationObserver.observe(this.element, config); + } + + /** + * Normalizes the options array to ensure consistent object structure + * @private + * @returns {Array} Array of normalized option objects with label, value, and disabled properties + */ + _normalizeOptions() { + return this._options.map((option) => { + if (typeof option === "string" || typeof option === "number") { + return { label: option, value: option, disabled: false }; + } + + return { + ...option, + disabled: option.disabled || false, + label: option.label, + value: option.value, + }; + }); + } + + /** + * Moves the thumb element to match the position and size of the selected segment + * @private + * @param {HTMLElement} segmentElement - The DOM element of the selected segment + */ + _moveThumbToSegment(segmentElement) { + if (!segmentElement || !this.thumbElement) return; + + const height = segmentElement.offsetHeight; + const width = segmentElement.offsetWidth; + + this.thumbElement.style.height = `${height}px`; + this.thumbElement.style.width = `${width}px`; + + if (this._orientation === VisualOrientation.horizontal) { + const left = segmentElement.offsetLeft; + this.thumbElement.style.transform = `translate3d(${left}px, 0, 0)`; + } else { + const top = segmentElement.offsetTop; + this.thumbElement.style.transform = `translate3d(0, ${top}px, 0)`; + } + } + + /** + * Applies CSS classes based on the current orientation + * @private + */ + _applyOrientationClasses() { + this.element.classList.remove(...Object.values(VisualOrientation.values)); + this.element.classList.add(this._orientation); + } + + /** + * Applies CSS classes based on the current shape + * @private + */ + _applyShapeClasses() { + this.element.classList.remove(...Object.values(VisualShape.values)); + this.element.classList.add(this._shape); + } + + /** + * Applies CSS classes based on the current size + * @private + */ + _applySizeClasses() { + this.element.classList.remove(...Object.values(VisualSize.values)); + this.element.classList.add(this._size); + } + + /** + * Applies or removes the disabled CSS class based on the disabled state + * @private + */ + _applyDisabledClass() { + if (this.disabled) { + this.element.classList.add("mod--disabled"); + } else { + this.element.classList.remove("mod--disabled"); + } + } +}; + +if (window.MontageElement) { + MontageElement.define("segmented-control-mod", SegmentedControl); +} + +exports.SegmentedControl = SegmentedControl; diff --git a/ui/segmented-control.mod/teach/assets/apple.svg b/ui/segmented-control.mod/teach/assets/apple.svg new file mode 100644 index 000000000..2e14fe35a --- /dev/null +++ b/ui/segmented-control.mod/teach/assets/apple.svg @@ -0,0 +1 @@ + diff --git a/ui/segmented-control.mod/teach/assets/banana.svg b/ui/segmented-control.mod/teach/assets/banana.svg new file mode 100644 index 000000000..8b4bd2c8f --- /dev/null +++ b/ui/segmented-control.mod/teach/assets/banana.svg @@ -0,0 +1 @@ + diff --git a/ui/segmented-control.mod/teach/assets/grappe.svg b/ui/segmented-control.mod/teach/assets/grappe.svg new file mode 100644 index 000000000..59ca18c14 --- /dev/null +++ b/ui/segmented-control.mod/teach/assets/grappe.svg @@ -0,0 +1 @@ + diff --git a/ui/segmented-control.mod/teach/index.html b/ui/segmented-control.mod/teach/index.html new file mode 100644 index 000000000..6147c82b6 --- /dev/null +++ b/ui/segmented-control.mod/teach/index.html @@ -0,0 +1,20 @@ + + + + + + Teach SegmentBar Mod + + + + + + + + diff --git a/ui/segmented-control.mod/teach/package.json b/ui/segmented-control.mod/teach/package.json new file mode 100644 index 000000000..98815d77d --- /dev/null +++ b/ui/segmented-control.mod/teach/package.json @@ -0,0 +1,10 @@ +{ + "name": "teach-segmented-bar-mod", + "private": true, + "dependencies": { + "mod": "*" + }, + "mappings": { + "mod": "../../../" + } +} diff --git a/ui/segmented-control.mod/teach/ui/main.mod/main.css b/ui/segmented-control.mod/teach/ui/main.mod/main.css new file mode 100644 index 000000000..4b9674806 --- /dev/null +++ b/ui/segmented-control.mod/teach/ui/main.mod/main.css @@ -0,0 +1,71 @@ +body { + font-family: -apple-system, Roboto, sans-serif; + background-color: #fafafa; + max-width: 800px; + margin: 0 auto; + padding: 20px; + + .card { + margin: 40px 0; + padding: 16px; + background: white; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + display: flex; + flex-direction: column; + gap: 24px; + + h2 { + margin: 0; + color: #333; + } + + label { + min-width: 128px; + font-weight: 500; + } + } + + .row { + display: flex; + align-items: center; + gap: 24px; + } + + .column { + display: flex; + flex-direction: column; + gap: 24px; + align-items: flex-start; + } + + .grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 16px; + } + + .customStyleSegmentedControl { + /* Container */ + --mod-segmented-control-background: rgba(71, 85, 105, 0.12); + + /* Thumb */ + --mod-segmented-control-thumb-background: rgba(71, 85, 105, 0.85); + --mod-segmented-control-thumb-border-color: rgba(71, 85, 105, 0.15); + --mod-segmented-control-thumb-shadow: 0px 0px 5px 1px rgba(71, 85, 105, 0.2); + --mod-segmented-control-thumb-text: rgba(255, 255, 255, 0.95); + + /* Segment */ + --mod-segment-text: rgba(71, 85, 105, 0.75); + + /* Segment Hover State */ + --mod-segment-hover-background: rgba(71, 85, 105, 0.08); + --mod-segment-hover-text: rgba(71, 85, 105, 0.9); + + /* Segment Active State */ + --mod-segment-active-background: rgba(71, 85, 105, 0.14); + --mod-segment-active-text: rgba(71, 85, 105, 1); + + --mod-segment-height-small: 28px; + } +} diff --git a/ui/segmented-control.mod/teach/ui/main.mod/main.html b/ui/segmented-control.mod/teach/ui/main.mod/main.html new file mode 100644 index 000000000..0b87540c4 --- /dev/null +++ b/ui/segmented-control.mod/teach/ui/main.mod/main.html @@ -0,0 +1,306 @@ + + + + + Teach SegmentedBar Mod + + + + +
+

Segmented Control

+ + +
+

Basic Usage

+
+ +
+
+
+ +
+
+
+ + +
+

Sizes

+
+ +
+
+
+ +
+
+
+ +
+
+
+ + +
+

Shape

+
+ +
+
+
+ +
+
+
+ +
+
+
+ + +
+

Vertical Orientation

+
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+ + +
+

Disabled Segments

+
+ +
+
+
+ +
+
+
+ + +
+

Custom Style

+
+ +
+
+
+ + +
+

Icon Segmented Control

+
+ +
+
+
+
+ + diff --git a/ui/segmented-control.mod/teach/ui/main.mod/main.js b/ui/segmented-control.mod/teach/ui/main.mod/main.js new file mode 100644 index 000000000..ae1a74557 --- /dev/null +++ b/ui/segmented-control.mod/teach/ui/main.mod/main.js @@ -0,0 +1,16 @@ +const { Component } = require("mod/ui/component"); + +exports.Main = class Main extends Component { + stringOptions = ["Apple", "Banana", "Cherry", "Orange"]; + objectOptions = [ + { label: "Daily", value: "daily" }, + { label: "Weekly", value: "weekly" }, + { label: "Monthly", value: "monthly" } + ]; + + mixedDisabledOptions = [ + { label: "Option 1", value: "option1" }, + { label: "Option 2", value: "option2", disabled: true }, + { label: "Option 3", value: "option3" } + ]; +}; From 5f8e82c7e5946d2b97fd8fab8aa280ee2b5a5e9d Mon Sep 17 00:00:00 2001 From: Thibault Zanini Date: Fri, 18 Jul 2025 17:34:44 +0200 Subject: [PATCH 12/21] chore: add fixme comments --- ui/placeholder.mod/placeholder.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ui/placeholder.mod/placeholder.js b/ui/placeholder.mod/placeholder.js index 34eb069cc..876874377 100755 --- a/ui/placeholder.mod/placeholder.js +++ b/ui/placeholder.mod/placeholder.js @@ -111,6 +111,13 @@ var Placeholder = exports.Placeholder = Slot.specialize({ oldExitDocument = component.exitDocument; component.data = self.data; + + // @Benoit FIXME: remove this when the component is properly bound + // START TESTING + component.defineBindings(self.componentValues); + component.src = self.componentValues.src['=']; + // END TESTING + component.exitDocument = function () { if (oldExitDocument) { oldExitDocument.call(component); From 70b7aee3d63d8108fb03035ae334ecb9ba4587ff Mon Sep 17 00:00:00 2001 From: Thibault Zanini Date: Mon, 21 Jul 2025 10:55:17 +0200 Subject: [PATCH 13/21] feat: bunch of improvements for the SegmentedControl component --- ui/segmented-control.mod/segmented-control.css | 10 +++++++++- ui/segmented-control.mod/segmented-control.html | 4 ++-- ui/segmented-control.mod/teach/ui/main.mod/main.css | 8 ++++++++ ui/segmented-control.mod/teach/ui/main.mod/main.html | 4 ++-- 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/ui/segmented-control.mod/segmented-control.css b/ui/segmented-control.mod/segmented-control.css index f6d07d969..9f554b37b 100644 --- a/ui/segmented-control.mod/segmented-control.css +++ b/ui/segmented-control.mod/segmented-control.css @@ -26,12 +26,13 @@ @scope (.ModSegmentedControl) { :scope { - display: inline-block; + display: inline-flex; -webkit-tap-highlight-color: transparent; &.mod--disabled { opacity: 0.6; pointer-events: none; + cursor: not-allowed; } &.mod--ready { @@ -49,6 +50,7 @@ background-color: var(--mod-segmented-control-background); padding: var(--mod-segmented-control-padding); position: relative; + flex: 1; .ModSegmentedControl-thumb { position: absolute; @@ -68,6 +70,12 @@ flex-wrap: nowrap; overflow: hidden; position: relative; + flex: 1; + justify-content: space-around; + + .ModSegmentedControl-segment { + flex: 1; + } } } } diff --git a/ui/segmented-control.mod/segmented-control.html b/ui/segmented-control.mod/segmented-control.html index 59da24dd4..724d3f747 100644 --- a/ui/segmented-control.mod/segmented-control.html +++ b/ui/segmented-control.mod/segmented-control.html @@ -22,8 +22,8 @@ "prototype": "ui/repetition.mod", "values": { "contentController": {"<-": "@optionsController"}, - "element": {"#": "segments"}, - "isSelectionEnabled": true + "isSelectionEnabled": {"<-": "!@owner.disabled"}, + "element": {"#": "segments"} } }, "segment": { diff --git a/ui/segmented-control.mod/teach/ui/main.mod/main.css b/ui/segmented-control.mod/teach/ui/main.mod/main.css index 4b9674806..e9f3688ff 100644 --- a/ui/segmented-control.mod/teach/ui/main.mod/main.css +++ b/ui/segmented-control.mod/teach/ui/main.mod/main.css @@ -37,6 +37,10 @@ body { flex-direction: column; gap: 24px; align-items: flex-start; + + &.stretch { + align-items: stretch; + } } .grid { @@ -67,5 +71,9 @@ body { --mod-segment-active-text: rgba(71, 85, 105, 1); --mod-segment-height-small: 28px; + + .ModSegmentedControl { + flex: 1; + } } } diff --git a/ui/segmented-control.mod/teach/ui/main.mod/main.html b/ui/segmented-control.mod/teach/ui/main.mod/main.html index 0b87540c4..9062c54fd 100644 --- a/ui/segmented-control.mod/teach/ui/main.mod/main.html +++ b/ui/segmented-control.mod/teach/ui/main.mod/main.html @@ -194,7 +194,7 @@ ], "orientation": "horizontal", "selection": "apple", - "size": "small", + "size": "medium", "shape": "pill" } } @@ -285,7 +285,7 @@

Disabled Segments

-
+

Custom Style

From 9ee0180ddfaeba609b551c3a9a029d0eb4725ab3 Mon Sep 17 00:00:00 2001 From: Thibault Zanini Date: Thu, 24 Jul 2025 16:21:48 +0200 Subject: [PATCH 14/21] feat: bunch of improvements for the SegmentedControl component --- .../segment.mod/segment.html | 12 +++---- .../segmented-control.html | 3 +- ui/segmented-control.mod/segmented-control.js | 34 +++---------------- ui/segmented-control.mod/teach/index.html | 32 ++++++++--------- .../teach/ui/main.mod/main.html | 33 +++++++++++------- ui/slot.mod/slot.js | 1 + 6 files changed, 50 insertions(+), 65 deletions(-) diff --git a/ui/segmented-control.mod/segment.mod/segment.html b/ui/segmented-control.mod/segment.mod/segment.html index 1f824d9b4..4f1ecf4c4 100644 --- a/ui/segmented-control.mod/segment.mod/segment.html +++ b/ui/segmented-control.mod/segment.mod/segment.html @@ -10,10 +10,10 @@ } }, "leading": { - "prototype": "ui/placeholder.mod", + "prototype": "ui/slot.mod", "values": { - "componentModule": {"<-": "@owner.option.leadingComponentModule"}, - "componentValues": {"<-": "@owner.option.leadingComponentValues"}, + "content": {"<-": "@owner.option.leadingComponent"}, + "delegate": {"<-": "@owner.delegate"}, "element": {"#": "leading"} } }, @@ -25,10 +25,10 @@ } }, "trailing": { - "prototype": "ui/placeholder.mod", + "prototype": "ui/slot.mod", "values": { - "componentModule": {"<-": "@owner.option.trailingComponentModule"}, - "componentValues": {"<-": "@owner.option.trailingComponentValues"}, + "content": {"<-": "@owner.option.trailingComponent"}, + "delegate": {"<-": "@owner.delegate"}, "element": {"#": "trailing"} } } diff --git a/ui/segmented-control.mod/segmented-control.html b/ui/segmented-control.mod/segmented-control.html index 724d3f747..dc5aec37f 100644 --- a/ui/segmented-control.mod/segmented-control.html +++ b/ui/segmented-control.mod/segmented-control.html @@ -31,7 +31,8 @@ "values": { "selected": {"<-": "@segments:iteration.object == @owner._selectedOption"}, "option": {"<-": "@segments:iteration.object"}, - "element": {"#": "segment"} + "element": {"#": "segment"}, + "delegate": {"<-": "@owner"} } } } diff --git a/ui/segmented-control.mod/segmented-control.js b/ui/segmented-control.mod/segmented-control.js index aa1c4902d..04a18122e 100644 --- a/ui/segmented-control.mod/segmented-control.js +++ b/ui/segmented-control.mod/segmented-control.js @@ -133,13 +133,15 @@ const SegmentedControl = class SegmentedControl extends Component { enterDocument() { this._cancelHandleOptionsChange = this.addRangeAtPathChangeListener("_options", this, "handleOptionsChange"); this.addPathChangeListener("_selectedOption", this, "handleSelectionChange"); - this._watchForDOMChanges(); } exitDocument() { this.removePathChangeListener("_selectedOption", this); this._cancelHandleOptionsChange?.(); - this._mutationObserver.disconnect(); + } + + slotContentDidFirstDraw(slot) { + this.needsDraw = true; } /** @@ -193,34 +195,6 @@ const SegmentedControl = class SegmentedControl extends Component { } } - // FIXME: See with @Benoit if we can avoid to render the segments when nested placeholders are not ready - // Draw Gates Maybe? - // Meanwhile, we need to watch for DOM changes - _watchForDOMChanges() { - this._mutationObserver = new MutationObserver((mutations) => { - mutations.some((mutation) => { - if (mutation.type === "childList") { - this.needsDraw = true; - return true; // Stop observing further mutations - } - - return false; // Continue observing - }); - }); - - // Configure what to observe - const config = { - characterDataOldValue: false, - attributeOldValue: false, - characterData: false, - attributes: false, - childList: true, - subtree: true, - }; - - this._mutationObserver.observe(this.element, config); - } - /** * Normalizes the options array to ensure consistent object structure * @private diff --git a/ui/segmented-control.mod/teach/index.html b/ui/segmented-control.mod/teach/index.html index 6147c82b6..ad0d6c62b 100644 --- a/ui/segmented-control.mod/teach/index.html +++ b/ui/segmented-control.mod/teach/index.html @@ -1,20 +1,20 @@ - - - - Teach SegmentBar Mod + + + + Teach SegmentControl Mod - - - - - - + + + + + + diff --git a/ui/segmented-control.mod/teach/ui/main.mod/main.html b/ui/segmented-control.mod/teach/ui/main.mod/main.html index 9062c54fd..770e92323 100644 --- a/ui/segmented-control.mod/teach/ui/main.mod/main.html +++ b/ui/segmented-control.mod/teach/ui/main.mod/main.html @@ -171,24 +171,15 @@ "element": {"#": "iconSegmentedControl"}, "options": [ { - "leadingComponentModule": {"%": "mod/ui/image.mod"}, - "leadingComponentValues": { - "src": {"=": "assets/apple.svg"} - }, + "leadingComponent": {"@": "appleIcon"}, "value": "apple" }, { - "leadingComponentModule": {"%": "mod/ui/image.mod"}, - "leadingComponentValues": { - "src": {"=": "assets/banana.svg"} - }, + "leadingComponent": {"@": "bananaIcon"}, "value": "banana" }, { - "leadingComponentModule": {"%": "mod/ui/image.mod"}, - "leadingComponentValues": { - "src": {"=": "assets/grappe.svg"} - }, + "leadingComponent": {"@": "grappeIcon"}, "value": "grappe" } ], @@ -197,6 +188,24 @@ "size": "medium", "shape": "pill" } + }, + "appleIcon": { + "prototype": "mod/ui/image.mod", + "values": { + "src": "assets/apple.svg" + } + }, + "bananaIcon": { + "prototype": "mod/ui/image.mod", + "values": { + "src": "assets/banana.svg" + } + }, + "grappeIcon": { + "prototype": "mod/ui/image.mod", + "values": { + "src": "assets/grappe.svg" + } } } diff --git a/ui/slot.mod/slot.js b/ui/slot.mod/slot.js index edecf132b..c0e631d8a 100644 --- a/ui/slot.mod/slot.js +++ b/ui/slot.mod/slot.js @@ -52,6 +52,7 @@ exports.Slot = class Slot extends Component { if (this.respondsToDelegateMethod("slotElementForComponent")) { element = this.callDelegateMethod("slotElementForComponent", this, value, element); } + value.element = element; } else { element = value.element; From 9e4a565c07d4c660df7bc53cd4c63648f6b5e83a Mon Sep 17 00:00:00 2001 From: Thibault Zanini Date: Fri, 25 Jul 2025 14:48:45 +0200 Subject: [PATCH 15/21] feat: minor improvements --- ui/segmented-control.mod/segmented-control.js | 5 +++++ .../teach/ui/main.mod/main.html | 15 ++++++++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/ui/segmented-control.mod/segmented-control.js b/ui/segmented-control.mod/segmented-control.js index 04a18122e..6ff3c3640 100644 --- a/ui/segmented-control.mod/segmented-control.js +++ b/ui/segmented-control.mod/segmented-control.js @@ -226,6 +226,11 @@ const SegmentedControl = class SegmentedControl extends Component { const height = segmentElement.offsetHeight; const width = segmentElement.offsetWidth; + if (width === 0 || height === 0) { + console.warn("Segment element has zero width or height, cannot position thumb."); + return; + } + this.thumbElement.style.height = `${height}px`; this.thumbElement.style.width = `${width}px`; diff --git a/ui/segmented-control.mod/teach/ui/main.mod/main.html b/ui/segmented-control.mod/teach/ui/main.mod/main.html index 770e92323..8a69f5f1e 100644 --- a/ui/segmented-control.mod/teach/ui/main.mod/main.html +++ b/ui/segmented-control.mod/teach/ui/main.mod/main.html @@ -192,19 +192,28 @@ "appleIcon": { "prototype": "mod/ui/image.mod", "values": { - "src": "assets/apple.svg" + "src": "assets/apple.svg", + "height": "20px", + "width": "20px", + "loading": "lazy" } }, "bananaIcon": { "prototype": "mod/ui/image.mod", "values": { - "src": "assets/banana.svg" + "src": "assets/banana.svg", + "height": "20px", + "width": "20px", + "loading": "lazy" } }, "grappeIcon": { "prototype": "mod/ui/image.mod", "values": { - "src": "assets/grappe.svg" + "src": "assets/grappe.svg", + "height": "20px", + "width": "20px", + "loading": "lazy" } } } From 87ffe0d6d1465966bc220bb24a5681a1e4a2edf0 Mon Sep 17 00:00:00 2001 From: Thibault Zanini Date: Fri, 25 Jul 2025 15:21:15 +0200 Subject: [PATCH 16/21] feat: minor css optimizations --- .../segmented-control.css | 126 +++++++++--------- 1 file changed, 63 insertions(+), 63 deletions(-) diff --git a/ui/segmented-control.mod/segmented-control.css b/ui/segmented-control.mod/segmented-control.css index 9f554b37b..9046af115 100644 --- a/ui/segmented-control.mod/segmented-control.css +++ b/ui/segmented-control.mod/segmented-control.css @@ -28,54 +28,54 @@ :scope { display: inline-flex; -webkit-tap-highlight-color: transparent; + } - &.mod--disabled { - opacity: 0.6; + > .ModSegmentedControl-container { + display: flex; + background-color: var(--mod-segmented-control-background); + padding: var(--mod-segmented-control-padding); + position: relative; + flex: 1; + + > .ModSegmentedControl-thumb { + position: absolute; + top: var(--mod-segmented-control-padding); + left: var(--mod-segmented-control-padding); + background-color: var(--mod-segmented-control-thumb-background); + backdrop-filter: blur(10px); + border: 2px solid var(--mod-segmented-control-thumb-border-color); + box-sizing: border-box; + box-shadow: var(--mod-segmented-control-thumb-shadow); + z-index: 1; pointer-events: none; - cursor: not-allowed; - } - - &.mod--ready { - /* This class is added after the first draw */ - /* FIXME: @benoit: maybe a better way of doing that, draw gates maybe ? */ - .ModSegmentedControl-container { - .ModSegmentedControl-thumb { - transition: var(--mod-segmented-control-thumb-transition); - } - } } - .ModSegmentedControl-container { + > .ModSegmentedControl-segments { display: flex; - background-color: var(--mod-segmented-control-background); - padding: var(--mod-segmented-control-padding); + flex-wrap: nowrap; + overflow: hidden; position: relative; flex: 1; + justify-content: space-around; - .ModSegmentedControl-thumb { - position: absolute; - top: var(--mod-segmented-control-padding); - left: var(--mod-segmented-control-padding); - background-color: var(--mod-segmented-control-thumb-background); - backdrop-filter: blur(10px); - border: 2px solid var(--mod-segmented-control-thumb-border-color); - box-sizing: border-box; - box-shadow: var(--mod-segmented-control-thumb-shadow); - z-index: 1; - pointer-events: none; - } - - .ModSegmentedControl-segments { - display: flex; - flex-wrap: nowrap; - overflow: hidden; - position: relative; + > .ModSegmentedControl-segment { flex: 1; - justify-content: space-around; + } + } + } - .ModSegmentedControl-segment { - flex: 1; - } + &.mod--disabled { + opacity: 0.6; + pointer-events: none; + cursor: not-allowed; + } + + &.mod--ready { + /* This class is added after the first draw */ + /* FIXME: @benoit: maybe a better way of doing that, draw gates maybe ? */ + > .ModSegmentedControl-container { + > .ModSegmentedControl-thumb { + transition: var(--mod-segmented-control-thumb-transition); } } } @@ -83,28 +83,28 @@ /* Orientation */ &.mod--horizontal { - .ModSegmentedControl-container { - .ModSegmentedControl-thumb { + > .ModSegmentedControl-container { + > .ModSegmentedControl-thumb { height: calc(100% - var(--mod-segmented-control-padding) * 2); } - .ModSegmentedControl-segments { + > .ModSegmentedControl-segments { flex-direction: row; } } } &.mod--vertical { - .ModSegmentedControl-container { - .ModSegmentedControl-thumb { + > .ModSegmentedControl-container { + > .ModSegmentedControl-thumb { width: calc(100% - var(--mod-segmented-control-padding) * 2); } - .ModSegmentedControl-segments { + > .ModSegmentedControl-segments { flex-direction: column; width: 100%; - .ModSegmentedControl-segment { + > .ModSegmentedControl-segment { width: 100%; text-align: left; } @@ -115,7 +115,7 @@ /* Size */ &.mod--size-small { - .ModSegmentedControl-container { + > .ModSegmentedControl-container { .ModSegmentedControl-segment { padding: var(--mod-segment-padding-small); min-height: var(--mod-segment-height-small); @@ -125,7 +125,7 @@ } &.mod--size-medium { - .ModSegmentedControl-container { + > .ModSegmentedControl-container { .ModSegmentedControl-segment { padding: var(--mod-segment-padding-medium); min-height: var(--mod-segment-height-medium); @@ -135,7 +135,7 @@ } &.mod--size-large { - .ModSegmentedControl-container { + > .ModSegmentedControl-container { .ModSegmentedControl-segment { padding: var(--mod-segment-padding-large); min-height: var(--mod-segment-height-large); @@ -150,19 +150,19 @@ &.mod--vertical { /* TODO: Consider handling this logic in JavaScript for greater resilience and flexibility */ &.mod--size-small { - .ModSegmentedControl-container { + > .ModSegmentedControl-container { border-radius: calc(var(--mod-segment-height-small) / 2 + var(--mod-segmented-control-padding)); } } &.mod--size-medium { - .ModSegmentedControl-container { + > .ModSegmentedControl-container { border-radius: calc(var(--mod-segment-height-medium) / 2 + var(--mod-segmented-control-padding)); } } &.mod--size-large { - .ModSegmentedControl-container { + > .ModSegmentedControl-container { border-radius: calc(var(--mod-segment-height-large) / 2 + var(--mod-segmented-control-padding)); } } @@ -171,12 +171,12 @@ &.mod--size-small, &.mod--size-medium, &.mod--size-large { - .ModSegmentedControl-container { + > .ModSegmentedControl-container { /* 50% doesn't work for rectangle shape */ border-radius: 99999px; - .ModSegmentedControl-thumb, - .ModSegmentedControl-segment { + > .ModSegmentedControl-thumb, + > .ModSegmentedControl-segments > .ModSegmentedControl-segment { border-radius: 99999px; } } @@ -185,33 +185,33 @@ &.mod--shape-rounded { &.mod--size-small { - .ModSegmentedControl-container { + > .ModSegmentedControl-container { border-radius: var(--mod-segment-border-radius-small); - .ModSegmentedControl-thumb, - .ModSegmentedControl-segment { + > .ModSegmentedControl-thumb, + > .ModSegmentedControl-segments > .ModSegmentedControl-segment { border-radius: calc(var(--mod-segment-border-radius-small) - var(--mod-segmented-control-padding)); } } } &.mod--size-medium { - .ModSegmentedControl-container { + > .ModSegmentedControl-container { border-radius: var(--mod-segment-border-radius-medium); - .ModSegmentedControl-thumb, - .ModSegmentedControl-segment { + > .ModSegmentedControl-thumb, + > .ModSegmentedControl-segments > .ModSegmentedControl-segment { border-radius: calc(var(--mod-segment-border-radius-medium) - var(--mod-segmented-control-padding)); } } } &.mod--size-large { - .ModSegmentedControl-container { + > .ModSegmentedControl-container { border-radius: var(--mod-segment-border-radius-large); - .ModSegmentedControl-thumb, - .ModSegmentedControl-segment { + > .ModSegmentedControl-thumb, + > .ModSegmentedControl-segments > .ModSegmentedControl-segment { border-radius: calc(var(--mod-segment-border-radius-large) - var(--mod-segmented-control-padding)); } } From 2299e6ea992690ef504c704624292c40c6681032 Mon Sep 17 00:00:00 2001 From: Thibault Zanini Date: Fri, 25 Jul 2025 17:05:07 +0200 Subject: [PATCH 17/21] feat: enable animations after the first draw in SegmentedControl --- ui/segmented-control.mod/segmented-control.css | 2 +- ui/segmented-control.mod/segmented-control.js | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/ui/segmented-control.mod/segmented-control.css b/ui/segmented-control.mod/segmented-control.css index 9046af115..0ac932b1b 100644 --- a/ui/segmented-control.mod/segmented-control.css +++ b/ui/segmented-control.mod/segmented-control.css @@ -70,7 +70,7 @@ cursor: not-allowed; } - &.mod--ready { + &.mod--animate { /* This class is added after the first draw */ /* FIXME: @benoit: maybe a better way of doing that, draw gates maybe ? */ > .ModSegmentedControl-container { diff --git a/ui/segmented-control.mod/segmented-control.js b/ui/segmented-control.mod/segmented-control.js index 6ff3c3640..9ee5c02ee 100644 --- a/ui/segmented-control.mod/segmented-control.js +++ b/ui/segmented-control.mod/segmented-control.js @@ -190,8 +190,9 @@ const SegmentedControl = class SegmentedControl extends Component { } didDraw() { - if (this._completedFirstDraw) { - this.element.classList.add("mod--ready"); + if (!this._completedFirstDraw) { + // Allow animations after first draw + requestAnimationFrame(() => this.element?.classList.add("mod--animate")); } } From 2d6d86f4f6da32afa711067642dbd1f53396a39d Mon Sep 17 00:00:00 2001 From: Thibault Zanini Date: Fri, 25 Jul 2025 17:13:36 +0200 Subject: [PATCH 18/21] feat: add user-drag prevention for images in SegmentedControl --- ui/segmented-control.mod/segment.mod/segment.css | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ui/segmented-control.mod/segment.mod/segment.css b/ui/segmented-control.mod/segment.mod/segment.css index 82cdddf8e..82adb319b 100644 --- a/ui/segmented-control.mod/segment.mod/segment.css +++ b/ui/segmented-control.mod/segment.mod/segment.css @@ -57,8 +57,8 @@ z-index: 2; box-sizing: border-box; white-space: nowrap; - user-select: none; -webkit-user-select: none; + user-select: none; outline: none; border: none; background: transparent; @@ -86,8 +86,6 @@ pointer-events: none; } - /* TESTING */ - .ModSegment-leading, .ModSegment-trailing, .ModSegment-label { @@ -101,6 +99,10 @@ display: flex; align-items: center; justify-content: center; + + img { + -webkit-user-drag: none; + } } } } From d3b99d895b3b0881f506886eaf09c5ad107c8b6a Mon Sep 17 00:00:00 2001 From: Thibault Zanini Date: Fri, 25 Jul 2025 17:48:24 +0200 Subject: [PATCH 19/21] chore: minor improvements --- .../teach/ui/main.mod/main.css | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/ui/segmented-control.mod/teach/ui/main.mod/main.css b/ui/segmented-control.mod/teach/ui/main.mod/main.css index e9f3688ff..1594d417e 100644 --- a/ui/segmented-control.mod/teach/ui/main.mod/main.css +++ b/ui/segmented-control.mod/teach/ui/main.mod/main.css @@ -28,19 +28,35 @@ body { .row { display: flex; - align-items: center; gap: 24px; + + &.center { + align-items: center; + justify-content: center; + } + + &.justify { + justify-content: space-between; + } + + &.space-around { + justify-content: space-around; + } } .column { display: flex; flex-direction: column; - gap: 24px; align-items: flex-start; + gap: 24px; &.stretch { align-items: stretch; } + + &.center { + align-items: center; + } } .grid { From 0c6cae6c5f049ef1908b78b75301b67991c1762f Mon Sep 17 00:00:00 2001 From: Thibault Zanini Date: Mon, 28 Jul 2025 14:32:22 +0200 Subject: [PATCH 20/21] feat: minor css improvements --- ui/segmented-control.mod/segment.mod/segment.css | 4 ++-- ui/segmented-control.mod/segmented-control.css | 12 +++++++----- ui/segmented-control.mod/teach/ui/main.mod/main.css | 3 ++- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/ui/segmented-control.mod/segment.mod/segment.css b/ui/segmented-control.mod/segment.mod/segment.css index 82adb319b..7730efb02 100644 --- a/ui/segmented-control.mod/segment.mod/segment.css +++ b/ui/segmented-control.mod/segment.mod/segment.css @@ -9,11 +9,11 @@ --mod-segment-height-large: 44px; /* Segment Hover State */ - --mod-segment-hover-background: rgba(0, 0, 0, 0.02); + --mod-segment-hover-background: rgba(229, 231, 235, 0.55); --mod-segment-hover-text: rgba(0, 0, 0, 1); /* Segment Active State */ - --mod-segment-active-background: rgba(0, 0, 0, 0.04); + --mod-segment-active-background: rgba(229, 231, 235, 1); --mod-segment-active-text: rgba(0, 0, 0, 1); /* Segment Border Sizes */ diff --git a/ui/segmented-control.mod/segmented-control.css b/ui/segmented-control.mod/segmented-control.css index 0ac932b1b..a1cf90a1e 100644 --- a/ui/segmented-control.mod/segmented-control.css +++ b/ui/segmented-control.mod/segmented-control.css @@ -1,13 +1,14 @@ :root { - --mod-segmented-control-background: rgba(0, 0, 0, 0.06); + --mod-segmented-control-background: rgb(243, 244, 246); --mod-segmented-control-padding: 2px; /* Thumb */ - --mod-segmented-control-thumb-background: rgba(255, 255, 255, 0.85); - --mod-segmented-control-thumb-border-color: rgba(255, 255, 255, 0.975); + --mod-segmented-control-thumb-background-color: none; + --mod-segmented-control-thumb-background-gradient: linear-gradient(356deg,rgba(253, 253, 253, 1) 0%, rgba(255, 255, 255, 1) 100%); + --mod-segmented-control-thumb-border-color: rgba(255, 255, 255, 1); --mod-segmented-control-thumb-shadow: 0px 0px 3px 1px rgba(0, 0, 0, 0.04); --mod-segmented-control-thumb-text: rgba(0, 0, 0, 0.85); - --mod-segmented-control-thumb-transition: all 0.35s cubic-bezier(0.375, 0.075, 0, 1.125); + --mod-segmented-control-thumb-transition: all 0.35s cubic-bezier(0.375, 0.075, 0, 1.115); } /* Dark Theme Variables */ @@ -41,7 +42,8 @@ position: absolute; top: var(--mod-segmented-control-padding); left: var(--mod-segmented-control-padding); - background-color: var(--mod-segmented-control-thumb-background); + background-color: var(--mod-segmented-control-thumb-background-color); + background-image: var(--mod-segmented-control-thumb-background-gradient); backdrop-filter: blur(10px); border: 2px solid var(--mod-segmented-control-thumb-border-color); box-sizing: border-box; diff --git a/ui/segmented-control.mod/teach/ui/main.mod/main.css b/ui/segmented-control.mod/teach/ui/main.mod/main.css index 1594d417e..106e22d18 100644 --- a/ui/segmented-control.mod/teach/ui/main.mod/main.css +++ b/ui/segmented-control.mod/teach/ui/main.mod/main.css @@ -70,7 +70,8 @@ body { --mod-segmented-control-background: rgba(71, 85, 105, 0.12); /* Thumb */ - --mod-segmented-control-thumb-background: rgba(71, 85, 105, 0.85); + --mod-segmented-control-thumb-background-color: rgba(71, 85, 105, 0.85); + --mod-segmented-control-thumb-background-gradient: none; --mod-segmented-control-thumb-border-color: rgba(71, 85, 105, 0.15); --mod-segmented-control-thumb-shadow: 0px 0px 5px 1px rgba(71, 85, 105, 0.2); --mod-segmented-control-thumb-text: rgba(255, 255, 255, 0.95); From 38c0e57e420c907802302697758379c879b316cc Mon Sep 17 00:00:00 2001 From: Thibault Zanini Date: Mon, 28 Jul 2025 15:25:41 +0200 Subject: [PATCH 21/21] refactor: enum usage in visual shape and size --- core/enum.js | 2 +- core/enums/visual-shape.js | 13 +++++++++---- core/enums/visual-size.js | 13 +++++++++---- ui/segmented-control.mod/segmented-control.js | 12 ++++++------ 4 files changed, 25 insertions(+), 15 deletions(-) diff --git a/core/enum.js b/core/enum.js index 44a5519fc..1d5595568 100644 --- a/core/enum.js +++ b/core/enum.js @@ -223,7 +223,7 @@ exports.Enum = Montage.specialize( /** @lends Enum# */ { values: { get: function () { - return this._members.map( (member) => this[member]); + return this._members.map((member) => this[member]); } } }); diff --git a/core/enums/visual-shape.js b/core/enums/visual-shape.js index 72c791caf..34c19800e 100644 --- a/core/enums/visual-shape.js +++ b/core/enums/visual-shape.js @@ -1,9 +1,14 @@ const { Enum } = require("../enum"); +const shapes = ["rectangle", "rounded", "pill"]; +const classNames = shapes.map((shape) => `mod--shape-${shape}`); + /** * @typedef {"rectangle"|"rounded"|'pill'} VisualShape */ -exports.VisualShape= new Enum().initWithMembersAndValues( - ["rectangle", "rounded", "pill"], - ["mod--shape-rectangle", "mod--shape-rounded", "mod--shape-pill"] -); +const VisualShape = new Enum().initWithMembersAndValues(shapes, shapes); + +const VisualShapeClassNames = new Enum().initWithMembersAndValues(shapes, classNames); + +exports.VisualShapeClassNames = VisualShapeClassNames; +exports.VisualShape = VisualShape; diff --git a/core/enums/visual-size.js b/core/enums/visual-size.js index 684155e9d..45f6bf5da 100644 --- a/core/enums/visual-size.js +++ b/core/enums/visual-size.js @@ -1,9 +1,14 @@ const { Enum } = require("../enum"); +const sizes = ["small", "medium", "large"]; +const classNames = sizes.map((size) => `mod--size-${size}`); + /** * @typedef {"small"|"medium"|'large'} VisualSize */ -exports.VisualSize= new Enum().initWithMembersAndValues( - ["small", "medium", "large"], - ["mod--size-small", "mod--size-medium", "mod--size-large"] -); +const VisualSize = new Enum().initWithMembersAndValues(sizes, sizes); + +const VisualSizeClassNames = new Enum().initWithMembersAndValues(sizes, classNames); + +exports.VisualSizeClassNames = VisualSizeClassNames; +exports.VisualSize = VisualSize; diff --git a/ui/segmented-control.mod/segmented-control.js b/ui/segmented-control.mod/segmented-control.js index 9ee5c02ee..c0cbaa597 100644 --- a/ui/segmented-control.mod/segmented-control.js +++ b/ui/segmented-control.mod/segmented-control.js @@ -1,6 +1,6 @@ +const { VisualShape, VisualShapeClassNames } = require("core/enums/visual-shape"); +const { VisualSize, VisualSizeClassNames } = require("core/enums/visual-size"); const { VisualOrientation } = require("core/enums/visual-orientation"); -const { VisualShape } = require("core/enums/visual-shape"); -const { VisualSize } = require("core/enums/visual-size"); const { Component } = require("ui/component"); /** @@ -258,8 +258,8 @@ const SegmentedControl = class SegmentedControl extends Component { * @private */ _applyShapeClasses() { - this.element.classList.remove(...Object.values(VisualShape.values)); - this.element.classList.add(this._shape); + this.element.classList.remove(...Object.values(VisualShapeClassNames.values)); + this.element.classList.add(VisualShapeClassNames[this._shape]); } /** @@ -267,8 +267,8 @@ const SegmentedControl = class SegmentedControl extends Component { * @private */ _applySizeClasses() { - this.element.classList.remove(...Object.values(VisualSize.values)); - this.element.classList.add(this._size); + this.element.classList.remove(...Object.values(VisualSizeClassNames.values)); + this.element.classList.add(VisualSizeClassNames[this._size]); } /**