diff --git a/.eslintrc.cjs b/.eslintrc.cjs index afaf5db4..4315f4f0 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -29,4 +29,10 @@ module.exports = { // For the Paramable interface, v-model directives need type annotation 'vue/valid-v-model': 'off', }, + overrides: [ + { + files: ['src/components/MageExchangeA.vue'], + rules: {'max-len': 'off'}, + }, + ], } diff --git a/src/App.vue b/src/App.vue index d2f491b7..281778b4 100644 --- a/src/App.vue +++ b/src/App.vue @@ -26,9 +26,11 @@ * { box-sizing: border-box; font-family: var(--ns-font-main); - color: var(--ns-color-black); } + a { + color: inherit; + } html, body { margin: 0; @@ -60,23 +62,29 @@ /* Dimensions */ --ns-desktop-tab-width: 300px; + --ns-specimen-card-width: 216px; /* Breakpoint widths Default styles should be for vertical mobile devices (devices narrower than --ns-breakpoint-mobile) - Small devices (landscape phones) - @media (min-width: var(--ns-breakpoint-mobile)) { ... } + // Small devices (landscape phones) + @media (min-width: $mobile-breakpoint) { ... } + // Note var(...) expressions are not allowed in media queries, + // so it's necessary to use the global scss variables set up + // in src/assets/scss/_variables.scss directly, as shown - Medium devices (tablets) - @media (min-width: var(--ns-breakpoint-tablet)) { ... } + // Medium devices (tablets) + @media (min-width: $tablet-breakpoint) { ... } // Large devices (desktops) - @media (min-width: var(--ns-breakpoint-desktop)) { ... } + @media (min-width: $desktop-breakpoint) { ... } */ - --ns-breakpoint-mobile: 580px; - --ns-breakpoint-tablet: 800px; - --ns-breakpoint-desktop: 1200px; + --ns-breakpoint-mobile: $mobile-breakpoint; + --ns-breakpoint-tablet: $tablet-breakpoint; + /* Not actually used at the moment: + --ns-breakpoint-desktop: 1200px; + */ } /* Display font */ diff --git a/src/assets/scss/_variables.scss b/src/assets/scss/_variables.scss new file mode 100644 index 00000000..93255d26 --- /dev/null +++ b/src/assets/scss/_variables.scss @@ -0,0 +1,6 @@ +/* These variables will be included in every style section in the + entire project +*/ + +$tablet-breakpoint: 800px; +$mobile-breakpoint: 580px; diff --git a/src/components/MageExchangeA.vue b/src/components/MageExchangeA.vue new file mode 100644 index 00000000..66326f04 --- /dev/null +++ b/src/components/MageExchangeA.vue @@ -0,0 +1,49 @@ + + + + + diff --git a/src/components/ParamEditor.vue b/src/components/ParamEditor.vue index 464d91ce..b6de4d57 100644 --- a/src/components/ParamEditor.vue +++ b/src/components/ParamEditor.vue @@ -5,8 +5,20 @@ {{ error }}

-

{{ title }}

- {{ paramable.name }} +
+
+

Current {{ title }}

+
{{ paramable.name }}
+
+
+ +
+ Change {{ title }} +
+
+

{{ paramable.description }}

ParamableInterface + function resetStatuses( + items: {[key: string]: unknown}, + statuses: {[key: string]: ValidationStatus} + ) { + for (const item in items) statuses[item] = ValidationStatus.ok() + } + export default defineComponent({ name: 'ParamEditor', props: { @@ -61,39 +81,41 @@ required: true, }, }, + emits: ['changed', 'openSwitcher'], components: { + MageExchangeA, ParamField, }, + computed: { + sortedParams() { + const sortedParams: {[key: string]: ParamHierarchy} = {} + Object.keys(this.paramable.params).forEach(key => { + const param = this.paramable.params[key] + if (!param.visibleDependency) + sortedParams[key] = {param, children: {}} + }) + Object.keys(this.paramable.params).forEach(key => { + const param = this.paramable.params[key] + if (param.visibleDependency) + sortedParams[param.visibleDependency].children[key] = + param + }) + return sortedParams + }, + }, data() { - const paramStatuses: {[key: string]: ValidationStatus} = {} const status = ValidationStatus.ok() - - Object.keys(this.paramable.params).forEach( - key => (paramStatuses[key] = ValidationStatus.ok()) - ) - - const sortedParams: {[key: string]: ParamHierarchy} = {} - Object.keys(this.paramable.params).forEach(key => { - const param = this.paramable.params[key] - if (!param.visibleDependency) - sortedParams[key] = {param, children: {}} - }) - Object.keys(this.paramable.params).forEach(key => { - const param = this.paramable.params[key] - if (param.visibleDependency) - sortedParams[param.visibleDependency].children[key] = - param - }) - - return {paramStatuses, status, sortedParams} + const paramStatuses: {[key: string]: ValidationStatus} = {} + resetStatuses(this.paramable.params, paramStatuses) + return {paramStatuses, status} }, created() { + const pstatus = this.paramStatuses + resetStatuses(this.paramable.params, pstatus) let good = true for (const param in this.paramable.params) { - const newStatus = ValidationStatus.ok() - this.paramable.validateIndividual(param, newStatus) - this.paramStatuses[param] = newStatus - good &&= newStatus.isValid() + this.paramable.validateIndividual(param, pstatus[param]) + good &&= pstatus[param].isValid() } if (good) { // The argument '.' to validate below skips all @@ -124,24 +146,50 @@ return param.visiblePredicate(v as never) else return param.visibleValue! === v }, + openSwitcher() { + this.$emit('openSwitcher') + }, + }, + watch: { + paramable() { + resetStatuses(this.paramable.params, this.paramStatuses) + }, }, }) diff --git a/src/components/SwitcherModal.vue b/src/components/SwitcherModal.vue new file mode 100644 index 00000000..0cd2aa3d --- /dev/null +++ b/src/components/SwitcherModal.vue @@ -0,0 +1,311 @@ + + + + + diff --git a/src/components/Tab.vue b/src/components/Tab.vue index 46110a57..bc2bfb3c 100644 --- a/src/components/Tab.vue +++ b/src/components/Tab.vue @@ -318,14 +318,14 @@ height: fit-content; } .content { - padding: 16px; + padding: 0px 16px 16px 16px; width: 100%; overflow-y: scroll; overflow-x: hidden; max-width: 500px; } - // desktop styles - @media (min-width: 700px) { + // tablet & desktop styles + @media (min-width: $tablet-breakpoint) { .buttons { display: flex; justify-content: flex-end; @@ -364,7 +364,6 @@ } .content { - padding: 16px; position: absolute; top: 16px; background-color: var(--ns-color-white); diff --git a/src/shared/Paramable.ts b/src/shared/Paramable.ts index 1ee170d2..51668d9e 100644 --- a/src/shared/Paramable.ts +++ b/src/shared/Paramable.ts @@ -39,6 +39,12 @@ export interface ParamInterface { // a blank input in the UI will use the `default` property instead of // displaying an error to the user. required: boolean + // The placeholder text that appears in the entry box for the parameter + // when that box is empty. This is really only applicable to non-required + // parameters, because for required ones, that box is not allowed to be + // empty. The placeholder defaults to the string representation of the + // default value for the parameter. + placeholder?: string /* If you want the control for this parameter only to be visible when * some other parameter has a specific value (because it is otherwise * irrelevant), set this `visibleDependency` property to the name of @@ -366,10 +372,12 @@ export class Paramable const tentative = this.tentativeValues[prop] const status = ValidationStatus.ok() - typeFunctions[param.type].validate(tentative, status) + // No need to validate empty optional params + if (tentative || param.required) + typeFunctions[param.type].validate(tentative, status) if (status.isValid()) { - const realized = typeFunctions[param.type].realize(tentative) - if (realized === me[prop]) continue + // Skip any parameters that already produce the current value + if (me[prop] === realizeOne(param, tentative)) continue } // Circumventing typescript a bit as we do above diff --git a/src/shared/Specimen.ts b/src/shared/Specimen.ts index 9663437d..21699b0c 100644 --- a/src/shared/Specimen.ts +++ b/src/shared/Specimen.ts @@ -166,11 +166,8 @@ export class Specimen { * the key of the desired visualizer's export module */ set visualizerKey(visualizerKey: string) { - // TODO: Do we need to check if the previous visualizer is already - // inhabiting an HTML element and .depart it if so, before it is - // setup again? Or will garbage collection of the old visualizer take - // care of that? this._visualizerKey = visualizerKey + this._visualizer.depart(this.location!) this._visualizer = new vizMODULES[visualizerKey].visualizer( this._sequence ) diff --git a/src/shared/layoutUtilities.ts b/src/shared/layoutUtilities.ts new file mode 100644 index 00000000..7f1a863a --- /dev/null +++ b/src/shared/layoutUtilities.ts @@ -0,0 +1,9 @@ +/* Helper functions for laying out the user interface */ +export function isMobile() { + const tabletBreakpoint = parseInt( + window + .getComputedStyle(document.documentElement) + .getPropertyValue('--ns-breakpoint-tablet') + ) + return window.innerWidth < tabletBreakpoint +} diff --git a/src/views/Gallery.vue b/src/views/Gallery.vue index f8218d6c..13b27e92 100644 --- a/src/views/Gallery.vue +++ b/src/views/Gallery.vue @@ -23,60 +23,43 @@ keyboard_arrow_up
- +

Saved Specimens

- keyboard_arrow_up -
- + @@ -129,6 +112,7 @@ font-size: var(--ns-size-title); } h2 { + margin-top: 3ex; font-size: var(--ns-size-heading); } h3 { @@ -145,6 +129,7 @@ align-items: center; margin: 8px 0; } + #change-button { max-width: 100px; width: min-content; @@ -152,10 +137,12 @@ flex-direction: column; align-items: center; } + #change-icon { display: block; margin: auto; } + #change-text { font-size: var(--ns-size-mini); text-align: center; @@ -169,14 +156,6 @@ display: none; } - .gallery { - display: flex; - flex-wrap: wrap; - justify-content: left; - margin-top: 29px; - gap: 29px; - } - .arrow-up { transform: rotate(0deg); transition: transform 0.3s; @@ -201,7 +180,6 @@ #header { display: block; - margin-bottom: 16px; } .visualizer-bar { @@ -210,10 +188,5 @@ align-items: center; margin-bottom: 16px; } - - .gallery { - gap: 16px; - margin: 0 0 16px 0; - } } diff --git a/src/views/Scope.vue b/src/views/Scope.vue index 7f685c84..3f18f2d6 100644 --- a/src/views/Scope.vue +++ b/src/views/Scope.vue @@ -7,6 +7,26 @@
+ + { + dropzone.classList.add('empty') + }) + document.querySelectorAll('.tab').forEach((tab: Element) => { if (!(tab instanceof HTMLElement)) return if (tab.getAttribute('docked') === 'none') return @@ -183,9 +222,19 @@ '#' + tab.getAttribute('docked') + '-dropzone' ) if (!(dropzone instanceof HTMLElement)) return - + dropzone.classList.remove('empty') positionAndSizeTab(tab, dropzone) }) + + document + .querySelectorAll('.dropzone-container') + .forEach((container: Element) => { + if (container.querySelectorAll('.empty').length == 2) { + container.classList.add('empty') + } else { + container.classList.remove('empty') + } + }) } // selects a tab export function selectTab(tab: HTMLElement): void { @@ -213,14 +262,19 @@ -