From 93a87b56460393033ee672d31e8da12d57739395 Mon Sep 17 00:00:00 2001 From: newt Date: Tue, 22 Feb 2022 13:27:12 +0000 Subject: [PATCH 001/678] Add status check. --- src/services/entity_normalizer/entity_normalizer.service.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index f219c161f..bd7f7ea5c 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -380,7 +380,9 @@ export const parseNotification = (data) => { if (masto) { output.type = mastoDict[data.type] || data.type output.seen = data.pleroma.is_seen - output.status = isStatusNotification(output.type) ? parseStatus(data.status) : null + // TODO: null check should be a temporary fix, I guess. + // Investigate why backend does this. + output.status = isStatusNotification(output.type) && data.status !== null ? parseStatus(data.status) : null output.action = output.status // TODO: Refactor, this is unneeded output.target = output.type !== 'move' ? null From 1bf256b34b020622718d4dcdcae4153a6f75e060 Mon Sep 17 00:00:00 2001 From: Xnuk Shuman Date: Tue, 20 Dec 2022 02:03:35 +0900 Subject: [PATCH 002/678] use normal checkbox component label in announcement --- src/components/announcement_editor/announcement_editor.vue | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/announcement_editor/announcement_editor.vue b/src/components/announcement_editor/announcement_editor.vue index 0f29f9f70..32b05bb20 100644 --- a/src/components/announcement_editor/announcement_editor.vue +++ b/src/components/announcement_editor/announcement_editor.vue @@ -32,8 +32,9 @@ id="announcement-all-day" v-model="announcement.allDay" :disabled="disabled" - /> - + > + {{ $t('announcements.all_day_prompt') }} + From f8a0cd2dd3e298b2a771d786033a7c29df8dcbfc Mon Sep 17 00:00:00 2001 From: Xnuk Shuman Date: Tue, 20 Dec 2022 02:34:11 +0900 Subject: [PATCH 003/678] vertical centering the checkbox --- src/components/checkbox/checkbox.vue | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/components/checkbox/checkbox.vue b/src/components/checkbox/checkbox.vue index b6768d67f..d7839e822 100644 --- a/src/components/checkbox/checkbox.vue +++ b/src/components/checkbox/checkbox.vue @@ -39,20 +39,23 @@ export default { display: inline-block; min-height: 1.2em; + & > * { + vertical-align: middle; + } + &-indicator { + display: inline-block; position: relative; - padding-left: 1.2em; + width: 1.2em; + height: 1.2em; } &-indicator::before { position: absolute; - right: 0; - top: 0; + inset: 0; display: block; content: '✓'; transition: color 200ms; - width: 1.1em; - height: 1.1em; border-radius: $fallback--checkboxRadius; border-radius: var(--checkboxRadius, $fallback--checkboxRadius); box-shadow: 0px 0px 2px black inset; From 7d90c594fe9530e8f483c6912ffcb80319934a8c Mon Sep 17 00:00:00 2001 From: tusooa Date: Fri, 6 Jan 2023 13:52:49 -0500 Subject: [PATCH 004/678] Make in-reply-to i18n-friendly --- src/components/status/status.vue | 83 ++++++++++++++++++-------------- src/i18n/en.json | 2 + 2 files changed, 50 insertions(+), 35 deletions(-) diff --git a/src/components/status/status.vue b/src/components/status/status.vue index 82eb7ac61..2a3946101 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -257,44 +257,57 @@ v-if="isReply" class="glued-label reply-glued-label" > - - - + + - - {{ $t('status.reply_to') }} - - + + {{ $t('status.reply_to') }} + + + + diff --git a/src/i18n/en.json b/src/i18n/en.json index 1ee1147ad..e98b1d4c5 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -853,6 +853,8 @@ "unbookmark": "Unbookmark", "delete_confirm": "Do you really want to delete this status?", "reply_to": "Reply to", + "reply_to_with_icon": "{icon} {replyTo}", + "reply_to_with_arg": "{replyToWithIcon} {user}", "mentions": "Mentions", "replies_list": "Replies:", "replies_list_with_others": "Replies (+{numReplies} other): | Replies (+{numReplies} others):", From 1506d2421d9a16867c5d3f39fbbc650952c2849a Mon Sep 17 00:00:00 2001 From: tusooa Date: Sat, 28 Jan 2023 21:44:24 -0500 Subject: [PATCH 005/678] Get rid of * --- src/components/checkbox/checkbox.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/checkbox/checkbox.vue b/src/components/checkbox/checkbox.vue index cbebe5789..6c23d9e48 100644 --- a/src/components/checkbox/checkbox.vue +++ b/src/components/checkbox/checkbox.vue @@ -39,7 +39,7 @@ export default { display: inline-block; min-height: 1.2em; - & > * { + .checkbox-indicator, .label { vertical-align: middle; } From f7daaead6f16d87aef5a6a26fa8f8cfe7f7caf10 Mon Sep 17 00:00:00 2001 From: tusooa Date: Sat, 28 Jan 2023 21:54:08 -0500 Subject: [PATCH 006/678] Fix stylelint --- src/components/checkbox/checkbox.vue | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/checkbox/checkbox.vue b/src/components/checkbox/checkbox.vue index 6c23d9e48..32c8f79cd 100644 --- a/src/components/checkbox/checkbox.vue +++ b/src/components/checkbox/checkbox.vue @@ -39,7 +39,8 @@ export default { display: inline-block; min-height: 1.2em; - .checkbox-indicator, .label { + &-indicator, + & .label { vertical-align: middle; } From 0dd343f2d40939900d12f8c42c5c7d4d6be97f63 Mon Sep 17 00:00:00 2001 From: Brian Underwood Date: Tue, 7 Feb 2023 21:48:57 +0100 Subject: [PATCH 007/678] Specs for the gallery component --- test/unit/specs/components/gallery.spec.js | 276 +++++++++++++++++++++ 1 file changed, 276 insertions(+) create mode 100644 test/unit/specs/components/gallery.spec.js diff --git a/test/unit/specs/components/gallery.spec.js b/test/unit/specs/components/gallery.spec.js new file mode 100644 index 000000000..108e2b6bf --- /dev/null +++ b/test/unit/specs/components/gallery.spec.js @@ -0,0 +1,276 @@ +import Gallery from 'src/components/gallery/gallery.vue' + +describe.only('Gallery', () => { + let local + + it('attachments is falsey', () => { + local = { attachments: false } + expect(Gallery.computed.rows.call(local)).to.eql([]) + + local = { attachments: null } + expect(Gallery.computed.rows.call(local)).to.eql([]) + + local = { attachments: undefined } + expect(Gallery.computed.rows.call(local)).to.eql([]) + }) + + it('no attachments', () => { + local = { attachments: [] } + expect(Gallery.computed.rows.call(local)).to.eql([]) + }) + + it('one audio attachment', () => { + local = { + attachments: [ + { mimetype: 'audio/mpeg' } + ] + } + + expect(Gallery.computed.rows.call(local)).to.eql([ + { audio: true, items: [{ mimetype: 'audio/mpeg' }] } + ]) + }) + + it('one image attachment', () => { + local = { + attachments: [ + { mimetype: 'image/png' } + ] + } + + expect(Gallery.computed.rows.call(local)).to.eql([ + { items: [{ mimetype: 'image/png' }] } + ]) + }) + + it('one audio attachment and one image attachment', () => { + local = { + attachments: [ + { mimetype: 'audio/mpeg' }, + { mimetype: 'image/png' } + ] + } + + expect(Gallery.computed.rows.call(local)).to.eql([ + { audio: true, items: [{ mimetype: 'audio/mpeg' }] }, + { items: [{ mimetype: 'image/png' }] } + ]) + }) + + it('has "size" key set to "hide"', () => { + let local + local = { + attachments: [ + { mimetype: 'audio/mpeg' } + ], + size: 'hide' + } + + expect(Gallery.computed.rows.call(local)).to.eql([ + { minimal: true, items: [{ mimetype: 'audio/mpeg' }] } + ]) + + local = { + attachments: [ + { mimetype: 'image/jpg' }, + { mimetype: 'image/png' }, + { mimetype: 'image/jpg' }, + { mimetype: 'audio/mpeg' }, + { mimetype: 'image/png' }, + { mimetype: 'audio/mpeg' }, + { mimetype: 'image/jpg' }, + { mimetype: 'image/png' }, + { mimetype: 'image/jpg' } + ], + size: 'hide' + } + + // When defining `size: hide`, the `items` aren't + // grouped and `audio` isn't set + expect(Gallery.computed.rows.call(local)).to.eql([ + { minimal: true, items: [{ mimetype: 'image/jpg' }] }, + { minimal: true, items: [{ mimetype: 'image/png' }] }, + { minimal: true, items: [{ mimetype: 'image/jpg' }] }, + { minimal: true, items: [{ mimetype: 'audio/mpeg' }] }, + { minimal: true, items: [{ mimetype: 'image/png' }] }, + { minimal: true, items: [{ mimetype: 'audio/mpeg' }] }, + { minimal: true, items: [{ mimetype: 'image/jpg' }] }, + { minimal: true, items: [{ mimetype: 'image/png' }] }, + { minimal: true, items: [{ mimetype: 'image/jpg' }] } + ]) + }) + + // types other than image or audio should be `minimal` + it('non-image/audio', () => { + let local + local = { + attachments: [ + { mimetype: 'plain/text' } + ] + } + expect(Gallery.computed.rows.call(local)).to.eql([ + { minimal: true, items: [{ mimetype: 'plain/text' }] } + ]) + + // No grouping of non-image/audio items + local = { + attachments: [ + { mimetype: 'plain/text' }, + { mimetype: 'plain/text' }, + { mimetype: 'plain/text' } + ] + } + expect(Gallery.computed.rows.call(local)).to.eql([ + { minimal: true, items: [{ mimetype: 'plain/text' }] }, + { minimal: true, items: [{ mimetype: 'plain/text' }] }, + { minimal: true, items: [{ mimetype: 'plain/text' }] } + ]) + + local = { + attachments: [ + { mimetype: 'image/png' }, + { mimetype: 'plain/text' }, + { mimetype: 'image/jpg' }, + { mimetype: 'audio/mpeg' } + ] + } + // NOTE / TODO: When defining `size: hide`, the `items` aren't + // grouped and `audio` isn't set + expect(Gallery.computed.rows.call(local)).to.eql([ + { items: [{ mimetype: 'image/png' }] }, + { minimal: true, items: [{ mimetype: 'plain/text' }] }, + { items: [{ mimetype: 'image/jpg' }] }, + { audio: true, items: [{ mimetype: 'audio/mpeg' }] } + ]) + }) + + it('mixed attachments', () => { + local = { + attachments: [ + { mimetype: 'audio/mpeg' }, + { mimetype: 'image/png' }, + { mimetype: 'audio/mpeg' }, + { mimetype: 'image/jpg' }, + { mimetype: 'image/png' }, + { mimetype: 'image/jpg' }, + { mimetype: 'image/jpg' } + ] + } + + expect(Gallery.computed.rows.call(local)).to.eql([ + { audio: true, items: [{ mimetype: 'audio/mpeg' }] }, + { items: [{ mimetype: 'image/png' }] }, + { audio: true, items: [{ mimetype: 'audio/mpeg' }] }, + { items: [{ mimetype: 'image/jpg' }, { mimetype: 'image/png' }, { mimetype: 'image/jpg' }, { mimetype: 'image/jpg' }] } + ]) + + local = { + attachments: [ + { mimetype: 'image/jpg' }, + { mimetype: 'image/png' }, + { mimetype: 'image/jpg' }, + { mimetype: 'image/jpg' }, + { mimetype: 'audio/mpeg' }, + { mimetype: 'image/png' }, + { mimetype: 'audio/mpeg' } + ] + } + + expect(Gallery.computed.rows.call(local)).to.eql([ + { items: [{ mimetype: 'image/jpg' }, { mimetype: 'image/png' }, { mimetype: 'image/jpg' }] }, + { items: [{ mimetype: 'image/jpg' }] }, + { audio: true, items: [{ mimetype: 'audio/mpeg' }] }, + { items: [{ mimetype: 'image/png' }] }, + { audio: true, items: [{ mimetype: 'audio/mpeg' }] } + ]) + + local = { + attachments: [ + { mimetype: 'image/jpg' }, + { mimetype: 'image/png' }, + { mimetype: 'image/jpg' }, + { mimetype: 'image/jpg' }, + { mimetype: 'image/png' }, + { mimetype: 'image/png' }, + { mimetype: 'image/jpg' } + ] + } + + // Group by three-per-row, unless there's one dangling, then stick it on the end of the last row + // https://git.pleroma.social/pleroma/pleroma-fe/-/merge_requests/1785#note_98514 + expect(Gallery.computed.rows.call(local)).to.eql([ + { items: [{ mimetype: 'image/jpg' }, { mimetype: 'image/png' }, { mimetype: 'image/jpg' }] }, + { items: [{ mimetype: 'image/jpg' }, { mimetype: 'image/png' }, { mimetype: 'image/png' }, { mimetype: 'image/jpg' }] } + ]) + + local = { + attachments: [ + { mimetype: 'image/jpg' }, + { mimetype: 'image/png' }, + { mimetype: 'image/jpg' }, + { mimetype: 'image/jpg' }, + { mimetype: 'image/png' }, + { mimetype: 'image/png' }, + { mimetype: 'image/jpg' }, + { mimetype: 'image/png' } + ] + } + + expect(Gallery.computed.rows.call(local)).to.eql([ + { items: [{ mimetype: 'image/jpg' }, { mimetype: 'image/png' }, { mimetype: 'image/jpg' }] }, + { items: [{ mimetype: 'image/jpg' }, { mimetype: 'image/png' }, { mimetype: 'image/png' }] }, + { items: [{ mimetype: 'image/jpg' }, { mimetype: 'image/png' }] } + ]) + }) + + it('does not do grouping when grid is set', () => { + const attachments = [ + { mimetype: 'audio/mpeg' }, + { mimetype: 'image/png' }, + { mimetype: 'audio/mpeg' }, + { mimetype: 'image/jpg' }, + { mimetype: 'image/png' }, + { mimetype: 'image/jpg' }, + { mimetype: 'image/jpg' } + ] + + local = { grid: true, attachments } + + expect(Gallery.computed.rows.call(local)).to.eql([ + { grid: true, items: attachments } + ]) + }) + + it('limit is set', () => { + const attachments = [ + { mimetype: 'audio/mpeg' }, + { mimetype: 'image/png' }, + { mimetype: 'image/jpg' }, + { mimetype: 'audio/mpeg' }, + { mimetype: 'image/jpg' } + ] + + let local + local = { attachments, limit: 2 } + + expect(Gallery.computed.rows.call(local)).to.eql([ + { audio: true, items: [{ mimetype: 'audio/mpeg' }] }, + { items: [{ mimetype: 'image/png' }] } + ]) + + local = { attachments, limit: 3 } + + expect(Gallery.computed.rows.call(local)).to.eql([ + { audio: true, items: [{ mimetype: 'audio/mpeg' }] }, + { items: [{ mimetype: 'image/png' }, { mimetype: 'image/jpg' }] } + ]) + + local = { attachments, limit: 4 } + + expect(Gallery.computed.rows.call(local)).to.eql([ + { audio: true, items: [{ mimetype: 'audio/mpeg' }] }, + { items: [{ mimetype: 'image/png' }, { mimetype: 'image/jpg' }] }, + { audio: true, items: [{ mimetype: 'audio/mpeg' }] } + ]) + }) +}) From 22fd3afd160d27c9ab43fcc8d1270db455403f8e Mon Sep 17 00:00:00 2001 From: Sean King Date: Thu, 30 Mar 2023 15:30:03 -0600 Subject: [PATCH 008/678] Change selenium server path to use require instead of hardcoding it --- test/e2e/nightwatch.conf.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/nightwatch.conf.js b/test/e2e/nightwatch.conf.js index 4041c6989..fdd2fda29 100644 --- a/test/e2e/nightwatch.conf.js +++ b/test/e2e/nightwatch.conf.js @@ -9,7 +9,7 @@ module.exports = { selenium: { start_process: true, - server_path: 'node_modules/selenium-server/lib/runner/selenium-server-standalone-2.53.1.jar', + server_path: require('selenium-server').path, host: '127.0.0.1', port: 4444, cli_args: { From 8e64b1791bca4b34ec1988dda3fea99eb1715c1f Mon Sep 17 00:00:00 2001 From: Sean King Date: Thu, 30 Mar 2023 15:30:30 -0600 Subject: [PATCH 009/678] Add selenium server logs path to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 4df5ec838..0d5befd28 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ selenium-debug.log .idea/ config/local.json static/emoji.json +logs/ From c7303598df6c63420b4dcdd65290635fe48430f0 Mon Sep 17 00:00:00 2001 From: Pleroma Renovate Bot Date: Wed, 3 May 2023 09:08:20 +0000 Subject: [PATCH 010/678] Update dependency selenium-server to v3 --- package.json | 2 +- yarn.lock | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 169dd609c..0f5c6e0c2 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "postcss-scss": "^4.0.6", "sass": "1.60.0", "sass-loader": "13.2.2", - "selenium-server": "2.53.1", + "selenium-server": "3.141.59", "semver": "7.3.8", "serviceworker-webpack5-plugin": "2.0.0", "shelljs": "0.8.5", diff --git a/yarn.lock b/yarn.lock index 1ccc9b058..0636b3724 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8011,9 +8011,10 @@ schema-utils@^4.0.0: ajv-formats "^2.1.1" ajv-keywords "^5.0.0" -selenium-server@2.53.1: - version "2.53.1" - resolved "https://registry.yarnpkg.com/selenium-server/-/selenium-server-2.53.1.tgz#d681528812f3c2e0531a6b7e613e23bb02cce8a6" +selenium-server@3.141.59: + version "3.141.59" + resolved "https://registry.yarnpkg.com/selenium-server/-/selenium-server-3.141.59.tgz#cbefdf50aae636ee4c67b819532a8233ce3fd6b0" + integrity sha512-pL7T1YtAqOEXiBbTx0KdZMkE2U7PYucemd7i0nDLcxcR1APXYZlJfNr5hrvL3mZgwXb7AJEZPINzC6mDU3eP5g== selenium-webdriver@4.6.1: version "4.6.1" From 84cab03a1d2643481bdb686d839a95973788ae02 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Thu, 9 Nov 2023 08:27:58 +0100 Subject: [PATCH 011/678] Extract backend repository from nodeinfo Also removes extractCommit, could be kept when the forge is git.pleroma.social Partial fix of https://git.pleroma.social/pleroma/pleroma-fe/-/issues/1283 --- changelog.d/backend-repo-url.skip | 0 src/boot/after_store.js | 3 ++- src/components/settings_modal/tabs/version_tab.js | 7 +------ src/components/settings_modal/tabs/version_tab.vue | 2 +- src/modules/instance.js | 1 + src/services/version/version.service.js | 6 ------ .../specs/services/version/version.service.spec.js | 11 ----------- 7 files changed, 5 insertions(+), 25 deletions(-) create mode 100644 changelog.d/backend-repo-url.skip delete mode 100644 src/services/version/version.service.js delete mode 100644 test/unit/specs/services/version/version.service.spec.js diff --git a/changelog.d/backend-repo-url.skip b/changelog.d/backend-repo-url.skip new file mode 100644 index 000000000..e69de29bb diff --git a/src/boot/after_store.js b/src/boot/after_store.js index 395d48344..aa204f537 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -242,7 +242,7 @@ const resolveStaffAccounts = ({ store, accounts }) => { const getNodeInfo = async ({ store }) => { try { - const res = await preloadFetch('/nodeinfo/2.0.json') + const res = await preloadFetch('/nodeinfo/2.1.json') if (res.ok) { const data = await res.json() const metadata = data.metadata @@ -277,6 +277,7 @@ const getNodeInfo = async ({ store }) => { const software = data.software store.dispatch('setInstanceOption', { name: 'backendVersion', value: software.version }) + store.dispatch('setInstanceOption', { name: 'backendRepository', value: software.repository }) store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: software.name === 'pleroma' }) const priv = metadata.private diff --git a/src/components/settings_modal/tabs/version_tab.js b/src/components/settings_modal/tabs/version_tab.js index 616bdadf5..20a671735 100644 --- a/src/components/settings_modal/tabs/version_tab.js +++ b/src/components/settings_modal/tabs/version_tab.js @@ -1,22 +1,17 @@ -import { extractCommit } from 'src/services/version/version.service' - const pleromaFeCommitUrl = 'https://git.pleroma.social/pleroma/pleroma-fe/commit/' -const pleromaBeCommitUrl = 'https://git.pleroma.social/pleroma/pleroma/commit/' const VersionTab = { data () { const instance = this.$store.state.instance return { backendVersion: instance.backendVersion, + backendRepository: instance.backendRepository, frontendVersion: instance.frontendVersion } }, computed: { frontendVersionLink () { return pleromaFeCommitUrl + this.frontendVersion - }, - backendVersionLink () { - return pleromaBeCommitUrl + extractCommit(this.backendVersion) } } } diff --git a/src/components/settings_modal/tabs/version_tab.vue b/src/components/settings_modal/tabs/version_tab.vue index 0330d49f8..917a618e6 100644 --- a/src/components/settings_modal/tabs/version_tab.vue +++ b/src/components/settings_modal/tabs/version_tab.vue @@ -7,7 +7,7 @@
  • {{ backendVersion }}
  • diff --git a/src/modules/instance.js b/src/modules/instance.js index 034348ff1..f908f16c0 100644 --- a/src/modules/instance.js +++ b/src/modules/instance.js @@ -141,6 +141,7 @@ const defaultState = { // Version Information backendVersion: '', + backendRepository: '', frontendVersion: '', pollsAvailable: false, diff --git a/src/services/version/version.service.js b/src/services/version/version.service.js deleted file mode 100644 index 2e11bf3a1..000000000 --- a/src/services/version/version.service.js +++ /dev/null @@ -1,6 +0,0 @@ - -export const extractCommit = versionString => { - const regex = /-g(\w+)/i - const matches = versionString.match(regex) - return matches ? matches[1] : '' -} diff --git a/test/unit/specs/services/version/version.service.spec.js b/test/unit/specs/services/version/version.service.spec.js deleted file mode 100644 index 519145ee1..000000000 --- a/test/unit/specs/services/version/version.service.spec.js +++ /dev/null @@ -1,11 +0,0 @@ -import { extractCommit } from 'src/services/version/version.service.js' - -describe('extractCommit', () => { - it('return short commit hash following "-g" characters', () => { - expect(extractCommit('1.0.0-45-g5e7aeebc')).to.eql('5e7aeebc') - }) - - it('return short commit hash without branch name', () => { - expect(extractCommit('1.0.0-45-g5e7aeebc-branch')).to.eql('5e7aeebc') - }) -}) From 6c4c8fe51f0e2c4ce55f5915a2bc5434aef5e5e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Thu, 4 Jan 2024 22:46:04 +0100 Subject: [PATCH 012/678] Display quotes count on posts and add quotes list page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- changelog.d/quotes-count.add | 1 + src/boot/routes.js | 2 ++ .../quotes_timeline/quotes_timeline.js | 26 +++++++++++++++++++ .../quotes_timeline/quotes_timeline.vue | 10 +++++++ src/components/status/status.scss | 1 + src/components/status/status.vue | 13 ++++++++++ src/components/timeline/timeline.js | 3 +++ src/components/timeline_menu/timeline_menu.js | 3 ++- src/i18n/en.json | 4 ++- src/modules/api.js | 5 ++-- src/services/api/api.service.js | 9 ++++++- .../backend_interactor_service.js | 4 +-- .../entity_normalizer.service.js | 1 + .../timeline_fetcher.service.js | 8 +++--- 14 files changed, 80 insertions(+), 10 deletions(-) create mode 100644 changelog.d/quotes-count.add create mode 100644 src/components/quotes_timeline/quotes_timeline.js create mode 100644 src/components/quotes_timeline/quotes_timeline.vue diff --git a/changelog.d/quotes-count.add b/changelog.d/quotes-count.add new file mode 100644 index 000000000..86779b96b --- /dev/null +++ b/changelog.d/quotes-count.add @@ -0,0 +1 @@ +Display quotes count on posts and add quotes list page \ No newline at end of file diff --git a/src/boot/routes.js b/src/boot/routes.js index 2dc900e7e..31e3dbb07 100644 --- a/src/boot/routes.js +++ b/src/boot/routes.js @@ -25,6 +25,7 @@ import ListsTimeline from 'components/lists_timeline/lists_timeline.vue' import ListsEdit from 'components/lists_edit/lists_edit.vue' import NavPanel from 'src/components/nav_panel/nav_panel.vue' import AnnouncementsPage from 'components/announcements_page/announcements_page.vue' +import QuotesTimeline from '../components/quotes_timeline/quotes_timeline.vue' export default (store) => { const validateAuthenticatedRoute = (to, from, next) => { @@ -51,6 +52,7 @@ export default (store) => { { name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline }, { name: 'bookmarks', path: '/bookmarks', component: BookmarkTimeline }, { name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } }, + { name: 'quotes', path: '/notice/:id/quotes', component: QuotesTimeline }, { name: 'remote-user-profile-acct', path: '/remote-users/:_(@)?:username([^/@]+)@:hostname([^/@]+)', diff --git a/src/components/quotes_timeline/quotes_timeline.js b/src/components/quotes_timeline/quotes_timeline.js new file mode 100644 index 000000000..a5f42da56 --- /dev/null +++ b/src/components/quotes_timeline/quotes_timeline.js @@ -0,0 +1,26 @@ +import Timeline from '../timeline/timeline.vue' + +const QuotesTimeline = { + created () { + this.$store.commit('clearTimeline', { timeline: 'quotes' }) + this.$store.dispatch('startFetchingTimeline', { timeline: 'quotes', statusId: this.statusId }) + }, + components: { + Timeline + }, + computed: { + statusId () { return this.$route.params.id }, + timeline () { return this.$store.state.statuses.timelines.quotes } + }, + watch: { + statusId () { + this.$store.commit('clearTimeline', { timeline: 'quotes' }) + this.$store.dispatch('startFetchingTimeline', { timeline: 'quotes', statusId: this.statusId }) + } + }, + unmounted () { + this.$store.dispatch('stopFetchingTimeline', 'quotes') + } +} + +export default QuotesTimeline diff --git a/src/components/quotes_timeline/quotes_timeline.vue b/src/components/quotes_timeline/quotes_timeline.vue new file mode 100644 index 000000000..835abd12e --- /dev/null +++ b/src/components/quotes_timeline/quotes_timeline.vue @@ -0,0 +1,10 @@ + + + diff --git a/src/components/status/status.scss b/src/components/status/status.scss index 760c6ac1a..54c4b4ff5 100644 --- a/src/components/status/status.scss +++ b/src/components/status/status.scss @@ -398,6 +398,7 @@ font-weight: bolder; font-size: 1.1em; line-height: 1em; + color: var(--selectedPostText, $fallback--text); } &:hover .stat-title { diff --git a/src/components/status/status.vue b/src/components/status/status.vue index fa9d6f0b9..11dc1e2e8 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -506,6 +506,19 @@ + +
    + {{ $t('status.quotes') }} +
    + {{ statusFromGlobalRepository.quotes_count }} +
    +
    +
    diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js index 1050b87a6..6cb9d1f10 100644 --- a/src/components/timeline/timeline.js +++ b/src/components/timeline/timeline.js @@ -25,6 +25,7 @@ const Timeline = { 'title', 'userId', 'listId', + 'statusId', 'tag', 'embedded', 'count', @@ -121,6 +122,7 @@ const Timeline = { showImmediately, userId: this.userId, listId: this.listId, + statusId: this.statusId, tag: this.tag }) }, @@ -183,6 +185,7 @@ const Timeline = { showImmediately: true, userId: this.userId, listId: this.listId, + statusId: this.statusId, tag: this.tag }).then(({ statuses }) => { if (statuses && statuses.length === 0) { diff --git a/src/components/timeline_menu/timeline_menu.js b/src/components/timeline_menu/timeline_menu.js index 5a2a86c2f..c4586b327 100644 --- a/src/components/timeline_menu/timeline_menu.js +++ b/src/components/timeline_menu/timeline_menu.js @@ -19,7 +19,8 @@ export const timelineNames = () => { bookmarks: 'nav.bookmarks', dms: 'nav.dms', 'public-timeline': 'nav.public_tl', - 'public-external-timeline': 'nav.twkn' + 'public-external-timeline': 'nav.twkn', + quotes: 'nav.quotes' } } diff --git a/src/i18n/en.json b/src/i18n/en.json index f4c9de18b..96b84c90c 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -190,7 +190,8 @@ "mobile_notifications": "Open notifications (there are unread ones)", "mobile_notifications_close": "Close notifications", "mobile_notifications_mark_as_seen": "Mark all as seen", - "announcements": "Announcements" + "announcements": "Announcements", + "quotes": "Quotes" }, "notifications": { "broken_favorite": "Unknown status, searching for it…", @@ -996,6 +997,7 @@ "status": { "favorites": "Favorites", "repeats": "Repeats", + "quotes": "Quotes", "repeat_confirm": "Do you really want to repeat this status?", "repeat_confirm_title": "Repeat confirmation", "repeat_confirm_accept_button": "Repeat", diff --git a/src/modules/api.js b/src/modules/api.js index fee584e84..3dbead5b4 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -202,12 +202,13 @@ const api = { timeline = 'friends', tag = false, userId = false, - listId = false + listId = false, + statusId = false }) { if (store.state.fetchers[timeline]) return const fetcher = store.state.backendInteractor.startFetchingTimeline({ - timeline, store, userId, listId, tag + timeline, store, userId, listId, statusId, tag }) store.commit('addFetcher', { fetcherName: timeline, fetcher }) }, diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index bde2e1633..004698e72 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -108,6 +108,7 @@ const PLEROMA_POST_ANNOUNCEMENT_URL = '/api/v1/pleroma/admin/announcements' const PLEROMA_EDIT_ANNOUNCEMENT_URL = id => `/api/v1/pleroma/admin/announcements/${id}` const PLEROMA_DELETE_ANNOUNCEMENT_URL = id => `/api/v1/pleroma/admin/announcements/${id}` const PLEROMA_SCROBBLES_URL = id => `/api/v1/pleroma/accounts/${id}/scrobbles` +const PLEROMA_STATUS_QUOTES_URL = id => `/api/v1/pleroma/statuses/${id}/quotes` const PLEROMA_ADMIN_CONFIG_URL = '/api/pleroma/admin/config' const PLEROMA_ADMIN_DESCRIPTIONS_URL = '/api/pleroma/admin/config/descriptions' @@ -675,6 +676,7 @@ const fetchTimeline = ({ until = false, userId = false, listId = false, + statusId = false, tag = false, withMuted = false, replyVisibility = 'all', @@ -691,7 +693,8 @@ const fetchTimeline = ({ list: MASTODON_LIST_TIMELINE_URL, favorites: MASTODON_USER_FAVORITES_TIMELINE_URL, tag: MASTODON_TAG_TIMELINE_URL, - bookmarks: MASTODON_BOOKMARK_TIMELINE_URL + bookmarks: MASTODON_BOOKMARK_TIMELINE_URL, + quotes: PLEROMA_STATUS_QUOTES_URL } const isNotifications = timeline === 'notifications' const params = [] @@ -706,6 +709,10 @@ const fetchTimeline = ({ url = url(listId) } + if (timeline === 'quotes') { + url = url(statusId) + } + if (minId) { params.push(['min_id', minId]) } diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js index 62ee85496..8ceb897d9 100644 --- a/src/services/backend_interactor_service/backend_interactor_service.js +++ b/src/services/backend_interactor_service/backend_interactor_service.js @@ -5,8 +5,8 @@ import followRequestFetcher from '../../services/follow_request_fetcher/follow_r import listsFetcher from '../../services/lists_fetcher/lists_fetcher.service.js' const backendInteractorService = credentials => ({ - startFetchingTimeline ({ timeline, store, userId = false, listId = false, tag }) { - return timelineFetcher.startFetching({ timeline, store, credentials, userId, listId, tag }) + startFetchingTimeline ({ timeline, store, userId = false, listId = false, statusId = false, tag }) { + return timelineFetcher.startFetching({ timeline, store, credentials, userId, listId, statusId, tag }) }, fetchTimeline (args) { diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index 85da52234..4d671c5dd 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -329,6 +329,7 @@ export const parseStatus = (data) => { output.quote_id = pleroma.quote_id ? pleroma.quote_id : (output.quote ? output.quote.id : undefined) output.quote_url = pleroma.quote_url output.quote_visible = pleroma.quote_visible + output.quotes_count = pleroma.quotes_count } else { output.text = data.content output.summary = data.spoiler_text diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js index 8501907e4..2fed14bcb 100644 --- a/src/services/timeline_fetcher/timeline_fetcher.service.js +++ b/src/services/timeline_fetcher/timeline_fetcher.service.js @@ -24,6 +24,7 @@ const fetchAndUpdate = ({ showImmediately = false, userId = false, listId = false, + statusId = false, tag = false, until, since @@ -47,6 +48,7 @@ const fetchAndUpdate = ({ args.userId = userId args.listId = listId + args.statusId = statusId args.tag = tag args.withMuted = !hideMutedPosts if (loggedIn && ['friends', 'public', 'publicAndExternal'].includes(timeline)) { @@ -78,15 +80,15 @@ const fetchAndUpdate = ({ }) } -const startFetching = ({ timeline = 'friends', credentials, store, userId = false, listId = false, tag = false }) => { +const startFetching = ({ timeline = 'friends', credentials, store, userId = false, listId = false, statusId = false, tag = false }) => { const rootState = store.rootState || store.state const timelineData = rootState.statuses.timelines[camelCase(timeline)] const showImmediately = timelineData.visibleStatuses.length === 0 timelineData.userId = userId timelineData.listId = listId - fetchAndUpdate({ timeline, credentials, store, showImmediately, userId, listId, tag }) + fetchAndUpdate({ timeline, credentials, store, showImmediately, userId, listId, statusId, tag }) const boundFetchAndUpdate = () => - fetchAndUpdate({ timeline, credentials, store, userId, listId, tag }) + fetchAndUpdate({ timeline, credentials, store, userId, listId, statusId, tag }) return promiseInterval(boundFetchAndUpdate, 10000) } const timelineFetcher = { From 521d308a6c6777a45c94183751f3305ce23bdad3 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 18 Jan 2024 14:35:25 +0200 Subject: [PATCH 013/678] themes 3 initial work --- src/components/button.style.js | 18 +++++ src/components/icon.style.js | 3 + src/components/panel.style.js | 8 +++ src/components/text.style.js | 9 +++ src/components/underlay.style.js | 6 ++ .../theme_data/theme_data_3.service.js | 66 +++++++++++++++++++ 6 files changed, 110 insertions(+) create mode 100644 src/components/button.style.js create mode 100644 src/components/icon.style.js create mode 100644 src/components/panel.style.js create mode 100644 src/components/text.style.js create mode 100644 src/components/underlay.style.js create mode 100644 src/services/theme_data/theme_data_3.service.js diff --git a/src/components/button.style.js b/src/components/button.style.js new file mode 100644 index 000000000..8f2e8f829 --- /dev/null +++ b/src/components/button.style.js @@ -0,0 +1,18 @@ +export default { + name: 'Button', + states: { + hover: ':hover', + disabled: ':disabled', + pressed: ':active', + toggled: '.toggled' + }, + variants: { + danger: '.danger', + unstyled: '.unstyled', + sublime: '.sublime' + }, + validInnerComponents: [ + 'Text', + 'Icon' + ] +} diff --git a/src/components/icon.style.js b/src/components/icon.style.js new file mode 100644 index 000000000..1e2781d6c --- /dev/null +++ b/src/components/icon.style.js @@ -0,0 +1,3 @@ +export default { + name: 'Icon' +} diff --git a/src/components/panel.style.js b/src/components/panel.style.js new file mode 100644 index 000000000..1666d9231 --- /dev/null +++ b/src/components/panel.style.js @@ -0,0 +1,8 @@ +export default { + name: 'Panel', + validInnerComponents: [ + 'Text', + 'Icon', + 'Button' + ] +} diff --git a/src/components/text.style.js b/src/components/text.style.js new file mode 100644 index 000000000..f87268bbf --- /dev/null +++ b/src/components/text.style.js @@ -0,0 +1,9 @@ +export default { + name: 'Text', + states: { + faint: '.faint' + }, + variants: { + green: '/.greentext' + } +} diff --git a/src/components/underlay.style.js b/src/components/underlay.style.js new file mode 100644 index 000000000..bae9fc0b3 --- /dev/null +++ b/src/components/underlay.style.js @@ -0,0 +1,6 @@ +export default { + name: 'Panel', + validInnerComponents: [ + 'Panel' + ] +} diff --git a/src/services/theme_data/theme_data_3.service.js b/src/services/theme_data/theme_data_3.service.js new file mode 100644 index 000000000..3a6fd5526 --- /dev/null +++ b/src/services/theme_data/theme_data_3.service.js @@ -0,0 +1,66 @@ +import Underlay from 'src/components/underlay.style.js' +import Panel from 'src/components/panel.style.js' +import Button from 'src/components/button.style.js' +import Text from 'src/components/text.style.js' +import Icon from 'src/components/icon.style.js' + +const root = Underlay +const components = { + Underlay, + Panel, + Button, + Text, + Icon +} + +// This gives you an array of arrays of all possible unique (i.e. order-insensitive) combinations +const getAllPossibleCombinations = (array) => { + const combos = [array.map(x => [x])] + for (let comboSize = 2; comboSize <= array.length; comboSize++) { + const previous = combos[combos.length - 1] + const selfSet = new Set() + const newCombos = previous.map(self => { + self.forEach(x => selfSet.add(x)) + const nonSelf = array.filter(x => !selfSet.has(x)) + return nonSelf.map(x => [...self, x]) + }) + const flatCombos = newCombos.reduce((acc, x) => [...acc, ...x], []) + combos.push(flatCombos) + } + return combos.reduce((acc, x) => [...acc, ...x], []) +} + +export const init = () => { + const rootName = root.name + const rules = [] + + const processInnerComponent = (component, parent) => { + const { + validInnerComponents, + states: originalStates = {}, + variants: originalVariants = {} + } = component + + const states = { normal: '', ...originalStates } + const variants = { normal: '', ...originalVariants } + const innerComponents = validInnerComponents.map(name => components[name]) + + const stateCombinations = getAllPossibleCombinations(Object.keys(states)) + const stateVariantCombination = Object.keys(variants).map(variant => { + return stateCombinations.map(state => ({ variant, state })) + }).reduce((acc, x) => [...acc, ...x], []) + + stateVariantCombination.forEach(combination => { + rules.push(({ + parent, + component: component.name, + state: combination.state, + variant: combination.variant + })) + + innerComponents.forEach(innerComponent => processInnerComponent(innerComponent, combination)) + }) + } + + processInnerComponent(components[rootName]) +} From f6d3a66a5b7bcbbecc72a66532866ee096c7752d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Fri, 19 Jan 2024 19:01:11 +0100 Subject: [PATCH 014/678] shouldDisplayFavsAndRepeats MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- src/components/status/status.js | 3 +++ src/components/status/status.vue | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/status/status.js b/src/components/status/status.js index 129929a3b..9e6c95287 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -374,6 +374,9 @@ const Status = { hidePostStats () { return this.mergedConfig.hidePostStats }, + shouldDisplayFavsAndRepeats () { + return !this.hidePostStats && this.isFocused && (this.combinedFavsAndRepeatsUsers.length > 0 || this.statusFromGlobalRepository.quotes_count) + }, muteBotStatuses () { return this.mergedConfig.muteBotStatuses }, diff --git a/src/components/status/status.vue b/src/components/status/status.vue index 11dc1e2e8..54fcc60fb 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -478,7 +478,7 @@
    From 0729b529d7da2002f25039e1dad2732302009cf3 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Tue, 23 Jan 2024 00:43:46 +0200 Subject: [PATCH 015/678] some more stuff, generating CSS selectors from rules --- src/components/button.style.js | 1 + src/components/icon.style.js | 3 +- src/components/panel.style.js | 1 + src/components/text.style.js | 4 +- src/components/underlay.style.js | 3 +- src/services/theme_data/pleromafe.t3.js | 26 ++++++++ .../theme_data/theme_data_3.service.js | 60 +++++++++++++++---- .../services/theme_data/theme_data3.spec.js | 26 ++++++++ 8 files changed, 108 insertions(+), 16 deletions(-) create mode 100644 src/services/theme_data/pleromafe.t3.js create mode 100644 test/unit/specs/services/theme_data/theme_data3.spec.js diff --git a/src/components/button.style.js b/src/components/button.style.js index 8f2e8f829..1c229f438 100644 --- a/src/components/button.style.js +++ b/src/components/button.style.js @@ -1,5 +1,6 @@ export default { name: 'Button', + selector: '.btn', states: { hover: ':hover', disabled: ':disabled', diff --git a/src/components/icon.style.js b/src/components/icon.style.js index 1e2781d6c..732cf16f0 100644 --- a/src/components/icon.style.js +++ b/src/components/icon.style.js @@ -1,3 +1,4 @@ export default { - name: 'Icon' + name: 'Icon', + selector: '.icon' } diff --git a/src/components/panel.style.js b/src/components/panel.style.js index 1666d9231..d34d5434d 100644 --- a/src/components/panel.style.js +++ b/src/components/panel.style.js @@ -1,5 +1,6 @@ export default { name: 'Panel', + selector: '.panel', validInnerComponents: [ 'Text', 'Icon', diff --git a/src/components/text.style.js b/src/components/text.style.js index f87268bbf..2aa5e7454 100644 --- a/src/components/text.style.js +++ b/src/components/text.style.js @@ -1,9 +1,7 @@ export default { name: 'Text', + selector: '', states: { faint: '.faint' - }, - variants: { - green: '/.greentext' } } diff --git a/src/components/underlay.style.js b/src/components/underlay.style.js index bae9fc0b3..426df1d7b 100644 --- a/src/components/underlay.style.js +++ b/src/components/underlay.style.js @@ -1,5 +1,6 @@ export default { - name: 'Panel', + name: 'Underlay', + selector: '.underlay', validInnerComponents: [ 'Panel' ] diff --git a/src/services/theme_data/pleromafe.t3.js b/src/services/theme_data/pleromafe.t3.js new file mode 100644 index 000000000..b44a395ea --- /dev/null +++ b/src/services/theme_data/pleromafe.t3.js @@ -0,0 +1,26 @@ +export const sampleRules = [ + { + component: 'Underlay', + // variant: 'normal', + // state: 'normal' + directives: { + background: '#000', + opacity: 0.2 + } + }, + { + component: 'Panel', + directives: { + background: '#FFFFFF', + opacity: 0.9 + } + }, + { + component: 'Button', + directives: { + background: '#808080', + text: '#FFFFFF', + opacity: 0.5 + } + } +] diff --git a/src/services/theme_data/theme_data_3.service.js b/src/services/theme_data/theme_data_3.service.js index 3a6fd5526..39a9998d7 100644 --- a/src/services/theme_data/theme_data_3.service.js +++ b/src/services/theme_data/theme_data_3.service.js @@ -14,7 +14,7 @@ const components = { } // This gives you an array of arrays of all possible unique (i.e. order-insensitive) combinations -const getAllPossibleCombinations = (array) => { +export const getAllPossibleCombinations = (array) => { const combos = [array.map(x => [x])] for (let comboSize = 2; comboSize <= array.length; comboSize++) { const previous = combos[combos.length - 1] @@ -30,15 +30,52 @@ const getAllPossibleCombinations = (array) => { return combos.reduce((acc, x) => [...acc, ...x], []) } -export const init = () => { +export const ruleToSelector = (rule) => { + const component = components[rule.component] + const { states, variants, selector } = component + + const applicableStates = (rule.state.filter(x => x !== 'normal') || []).map(state => states[state]) + + const applicableVariantName = (rule.variant || 'normal') + let applicableVariant = '' + if (applicableVariantName !== 'normal') { + applicableVariant = variants[applicableVariantName] + } + + const selectors = [selector, applicableVariant, ...applicableStates] + .toSorted((a, b) => { + if (a.startsWith(':')) return 1 + else return -1 + }) + .join('') + + if (rule.parent) { + return ruleToSelector(rule.parent) + ' ' + selectors + } + return selectors +} + +export const init = (ruleset) => { const rootName = root.name const rules = [] + const rulesByComponent = {} + + const addRule = (rule) => { + rules.push(rule) + rulesByComponent[rule.component] = rulesByComponent[rule.component] || [] + rulesByComponent.push(rule) + } + + ruleset.forEach(rule => { + + }) const processInnerComponent = (component, parent) => { const { - validInnerComponents, + validInnerComponents = [], states: originalStates = {}, - variants: originalVariants = {} + variants: originalVariants = {}, + name } = component const states = { normal: '', ...originalStates } @@ -51,16 +88,17 @@ export const init = () => { }).reduce((acc, x) => [...acc, ...x], []) stateVariantCombination.forEach(combination => { - rules.push(({ - parent, - component: component.name, - state: combination.state, - variant: combination.variant - })) + // addRule(({ + // parent, + // component: component.name, + // state: combination.state, + // variant: combination.variant + // })) - innerComponents.forEach(innerComponent => processInnerComponent(innerComponent, combination)) + innerComponents.forEach(innerComponent => processInnerComponent(innerComponent, { parent, component: name, ...combination })) }) } processInnerComponent(components[rootName]) + return rules } diff --git a/test/unit/specs/services/theme_data/theme_data3.spec.js b/test/unit/specs/services/theme_data/theme_data3.spec.js new file mode 100644 index 000000000..e76200c00 --- /dev/null +++ b/test/unit/specs/services/theme_data/theme_data3.spec.js @@ -0,0 +1,26 @@ +// import { topoSort } from 'src/services/theme_data/theme_data.service.js' +import { + getAllPossibleCombinations, + init, + ruleToSelector +} from 'src/services/theme_data/theme_data_3.service.js' +import { + sampleRules +} from 'src/services/theme_data/pleromafe.t3.js' + +describe.only('Theme Data 3', () => { + describe('getAllPossibleCombinations', () => { + it('test simple case', () => { + const out = getAllPossibleCombinations([1, 2, 3]).map(x => x.sort((a, b) => a - b)) + expect(out).to.eql([[1], [2], [3], [1, 2], [1, 3], [2, 3], [1, 2, 3]]) + }) + }) + + describe('init', () => { + it('test simple case', () => { + const out = init(sampleRules) + console.log(JSON.stringify(out, null, 2)) + out.forEach(r => console.log(ruleToSelector(r))) + }) + }) +}) From 22b32f149d7753592fddcfb1cd2679b3fbac33d6 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Tue, 23 Jan 2024 19:18:55 +0200 Subject: [PATCH 016/678] shit more or less works for the very basic stuff --- src/components/button.style.js | 4 +- src/components/panel.style.js | 3 +- src/components/panel_header.style.js | 9 ++ src/components/text.style.js | 2 +- src/components/underlay.style.js | 2 +- src/services/color_convert/color_convert.js | 1 + src/services/theme_data/pleromafe.t3.js | 10 +- .../theme_data/theme_data_3.service.js | 116 ++++++++++++++++-- .../services/theme_data/theme_data3.spec.js | 7 +- 9 files changed, 132 insertions(+), 22 deletions(-) create mode 100644 src/components/panel_header.style.js diff --git a/src/components/button.style.js b/src/components/button.style.js index 1c229f438..51f781e14 100644 --- a/src/components/button.style.js +++ b/src/components/button.style.js @@ -2,10 +2,10 @@ export default { name: 'Button', selector: '.btn', states: { - hover: ':hover', disabled: ':disabled', + toggled: '.toggled', pressed: ':active', - toggled: '.toggled' + hover: ':hover' }, variants: { danger: '.danger', diff --git a/src/components/panel.style.js b/src/components/panel.style.js index d34d5434d..e3da4d1a0 100644 --- a/src/components/panel.style.js +++ b/src/components/panel.style.js @@ -4,6 +4,7 @@ export default { validInnerComponents: [ 'Text', 'Icon', - 'Button' + 'Button', + 'PanelHeader' ] } diff --git a/src/components/panel_header.style.js b/src/components/panel_header.style.js new file mode 100644 index 000000000..aaa8bea9f --- /dev/null +++ b/src/components/panel_header.style.js @@ -0,0 +1,9 @@ +export default { + name: 'PanelHeader', + selector: '.panel-heading', + validInnerComponents: [ + 'Text', + 'Icon', + 'Button' + ] +} diff --git a/src/components/text.style.js b/src/components/text.style.js index 2aa5e7454..050194cbe 100644 --- a/src/components/text.style.js +++ b/src/components/text.style.js @@ -1,6 +1,6 @@ export default { name: 'Text', - selector: '', + selector: '/*text*/', states: { faint: '.faint' } diff --git a/src/components/underlay.style.js b/src/components/underlay.style.js index 426df1d7b..be1ecc56b 100644 --- a/src/components/underlay.style.js +++ b/src/components/underlay.style.js @@ -1,6 +1,6 @@ export default { name: 'Underlay', - selector: '.underlay', + selector: '#app', validInnerComponents: [ 'Panel' ] diff --git a/src/services/color_convert/color_convert.js b/src/services/color_convert/color_convert.js index 47d6344e5..23e4ca611 100644 --- a/src/services/color_convert/color_convert.js +++ b/src/services/color_convert/color_convert.js @@ -188,6 +188,7 @@ export const rgba2css = function (rgba) { */ export const getTextColor = function (bg, text, preserve) { const contrast = getContrastRatio(bg, text) + console.log(contrast) if (contrast < 4.5) { const base = typeof text.a !== 'undefined' ? { a: text.a } : {} diff --git a/src/services/theme_data/pleromafe.t3.js b/src/services/theme_data/pleromafe.t3.js index b44a395ea..b7b125731 100644 --- a/src/services/theme_data/pleromafe.t3.js +++ b/src/services/theme_data/pleromafe.t3.js @@ -4,7 +4,7 @@ export const sampleRules = [ // variant: 'normal', // state: 'normal' directives: { - background: '#000', + background: '#000000', opacity: 0.2 } }, @@ -15,11 +15,17 @@ export const sampleRules = [ opacity: 0.9 } }, + { + component: 'PanelHeader', + directives: { + background: '#000000', + opacity: 0.9 + } + }, { component: 'Button', directives: { background: '#808080', - text: '#FFFFFF', opacity: 0.5 } } diff --git a/src/services/theme_data/theme_data_3.service.js b/src/services/theme_data/theme_data_3.service.js index 39a9998d7..d65d53527 100644 --- a/src/services/theme_data/theme_data_3.service.js +++ b/src/services/theme_data/theme_data_3.service.js @@ -1,5 +1,9 @@ +import { convert } from 'chromatism' +import { alphaBlend, getTextColor, rgba2css } from '../color_convert/color_convert.js' + import Underlay from 'src/components/underlay.style.js' import Panel from 'src/components/panel.style.js' +import PanelHeader from 'src/components/panel_header.style.js' import Button from 'src/components/button.style.js' import Text from 'src/components/text.style.js' import Icon from 'src/components/icon.style.js' @@ -8,6 +12,7 @@ const root = Underlay const components = { Underlay, Panel, + PanelHeader, Button, Text, Icon @@ -34,7 +39,7 @@ export const ruleToSelector = (rule) => { const component = components[rule.component] const { states, variants, selector } = component - const applicableStates = (rule.state.filter(x => x !== 'normal') || []).map(state => states[state]) + const applicableStates = ((rule.state || []).filter(x => x !== 'normal')).map(state => states[state]) const applicableVariantName = (rule.variant || 'normal') let applicableVariant = '' @@ -63,12 +68,37 @@ export const init = (ruleset) => { const addRule = (rule) => { rules.push(rule) rulesByComponent[rule.component] = rulesByComponent[rule.component] || [] - rulesByComponent.push(rule) + rulesByComponent[rule.component].push(rule) } - ruleset.forEach(rule => { + const findRules = (combination) => rule => { + if (combination.component !== rule.component) return false + if (Object.prototype.hasOwnProperty.call(rule, 'variant')) { + if (combination.variant !== rule.variant) return false + } else { + if (combination.variant !== 'normal') return false + } - }) + if (Object.prototype.hasOwnProperty.call(rule, 'state')) { + const ruleStatesSet = new Set(['normal', ...(rule.state || [])]) + return combination.state.every(state => ruleStatesSet.has(state)) + } else { + if (combination.state.length !== 1 || combination.state[0] !== 'normal') return false + return true + } + } + + const findLowerLevelRule = (parent, filter = () => true) => { + let lowerLevelComponent = null + let currentParent = parent + while (currentParent) { + const rulesParent = ruleset.filter(findRules(currentParent, true)) + lowerLevelComponent = rulesParent[rulesParent.length - 1] + currentParent = currentParent.parent + if (lowerLevelComponent && filter(lowerLevelComponent)) currentParent = null + } + return filter(lowerLevelComponent) ? lowerLevelComponent : null + } const processInnerComponent = (component, parent) => { const { @@ -87,18 +117,82 @@ export const init = (ruleset) => { return stateCombinations.map(state => ({ variant, state })) }).reduce((acc, x) => [...acc, ...x], []) + const VIRTUAL_COMPONENTS = new Set(['Text', 'Link', 'Icon']) + stateVariantCombination.forEach(combination => { - // addRule(({ - // parent, - // component: component.name, - // state: combination.state, - // variant: combination.variant - // })) + const existingRules = ruleset.filter(findRules({ component: component.name, ...combination })) + const lastRule = existingRules[existingRules.length - 1] + + if (existingRules.length !== 0) { + const { directives } = lastRule + const rgb = convert(directives.background).rgb + + // TODO: DEFAULT TEXT COLOR + const bg = findLowerLevelRule(parent)?.cache.background || convert('#FFFFFF').rgb + + if (!lastRule.cache?.background) { + const blend = directives.opacity < 1 ? alphaBlend(rgb, directives.opacity, bg) : rgb + lastRule.cache = lastRule.cache || {} + lastRule.cache.background = blend + + addRule(lastRule) + } + } else { + if (VIRTUAL_COMPONENTS.has(component.name)) { + const selector = component.name + ruleToSelector({ component: component.name, ...combination }) + + const lowerLevel = findLowerLevelRule(parent, (r) => { + if (components[r.component].validInnerComponents.indexOf(component.name) < 0) return false + if (r.cache?.background === undefined) return false + if (r.cache.textDefined) { + return !r.cache.textDefined[selector] + } + return true + }) + if (!lowerLevel) return + lowerLevel.cache.textDefined = lowerLevel.cache.textDefined || {} + lowerLevel.cache.textDefined[selector] = true + addRule({ + parent, + component: component.name, + ...combination, + directives: { + // TODO: DEFAULT TEXT COLOR + textColor: getTextColor(convert(lowerLevel.cache.background).rgb, convert('#FFFFFF').rgb, component.name === 'Link'), + // Debug: lets you see what it think background color should be + background: convert(lowerLevel.cache.background).hex + } + }) + } + } innerComponents.forEach(innerComponent => processInnerComponent(innerComponent, { parent, component: name, ...combination })) }) } processInnerComponent(components[rootName]) - return rules + + // console.info(rules.map(x => [ + // (parent?.component || 'root') + ' -> ' + x.component, + // // 'Cached background:' + convert(bg).hex, + // // 'Color: ' + convert(x.directives.background).hex + ' A:' + x.directives.opacity, + // JSON.stringify(x.directives) + // // '=> Blend: ' + convert(x.cache.background).hex + // ].join(' '))) + + return { + raw: rules, + css: rules.map(rule => { + const header = ruleToSelector(rule) + ' {' + const footer = '}' + const directives = Object.entries(rule.directives).map(([k, v]) => { + switch (k) { + case 'background': return 'background-color: ' + rgba2css({ ...convert(v).rgb, a: rule.directives.opacity ?? 1 }) + case 'textColor': return 'color: ' + rgba2css({ ...convert(v).rgb, a: rule.directives.opacity ?? 1 }) + default: return '' + } + }).filter(x => x).map(x => ' ' + x).join(';\n') + return [header, directives, footer].join('\n') + }) + } } diff --git a/test/unit/specs/services/theme_data/theme_data3.spec.js b/test/unit/specs/services/theme_data/theme_data3.spec.js index e76200c00..915bb5ce1 100644 --- a/test/unit/specs/services/theme_data/theme_data3.spec.js +++ b/test/unit/specs/services/theme_data/theme_data3.spec.js @@ -1,8 +1,7 @@ // import { topoSort } from 'src/services/theme_data/theme_data.service.js' import { getAllPossibleCombinations, - init, - ruleToSelector + init } from 'src/services/theme_data/theme_data_3.service.js' import { sampleRules @@ -19,8 +18,8 @@ describe.only('Theme Data 3', () => { describe('init', () => { it('test simple case', () => { const out = init(sampleRules) - console.log(JSON.stringify(out, null, 2)) - out.forEach(r => console.log(ruleToSelector(r))) + // console.log(JSON.stringify(out, null, 2)) + console.log('\n' + out.css.join('\n') + '\n') }) }) }) From ff2db7a247284cb32933b83c56286a942923a398 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Tue, 23 Jan 2024 20:39:52 +0200 Subject: [PATCH 017/678] fix states --- src/services/theme_data/pleromafe.t3.js | 12 ++++++++++-- src/services/theme_data/theme_data_3.service.js | 6 +++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/services/theme_data/pleromafe.t3.js b/src/services/theme_data/pleromafe.t3.js index b7b125731..8d8e19cd3 100644 --- a/src/services/theme_data/pleromafe.t3.js +++ b/src/services/theme_data/pleromafe.t3.js @@ -25,8 +25,16 @@ export const sampleRules = [ { component: 'Button', directives: { - background: '#808080', - opacity: 0.5 + background: '#000000', + opacity: 0.8 + } + }, + { + component: 'Button', + state: ['hover'], + directives: { + background: '#FF00FF', + opacity: 0.9 } } ] diff --git a/src/services/theme_data/theme_data_3.service.js b/src/services/theme_data/theme_data_3.service.js index d65d53527..2ef5e17fd 100644 --- a/src/services/theme_data/theme_data_3.service.js +++ b/src/services/theme_data/theme_data_3.service.js @@ -81,7 +81,10 @@ export const init = (ruleset) => { if (Object.prototype.hasOwnProperty.call(rule, 'state')) { const ruleStatesSet = new Set(['normal', ...(rule.state || [])]) - return combination.state.every(state => ruleStatesSet.has(state)) + const combinationSet = new Set(['normal', ...combination.state]) + const setsAreEqual = combination.state.every(state => ruleStatesSet.has(state)) && + [...ruleStatesSet].every(state => combinationSet.has(state)) + return setsAreEqual } else { if (combination.state.length !== 1 || combination.state[0] !== 'normal') return false return true @@ -93,6 +96,7 @@ export const init = (ruleset) => { let currentParent = parent while (currentParent) { const rulesParent = ruleset.filter(findRules(currentParent, true)) + rulesParent > 1 && console.log('OOPS') lowerLevelComponent = rulesParent[rulesParent.length - 1] currentParent = currentParent.parent if (lowerLevelComponent && filter(lowerLevelComponent)) currentParent = null From 53a4b1f9a6a9aa6bc044609c3accb074d924daf9 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 31 Jan 2024 17:39:51 +0200 Subject: [PATCH 018/678] better virtual components and stuff --- src/App.scss | 28 +- src/components/icon.style.js | 13 +- src/components/link.style.js | 25 ++ src/components/notification/notification.vue | 2 +- .../notifications/notifications.scss | 16 +- src/components/panel.style.js | 1 + src/components/panel_header.style.js | 1 + src/components/status/status.vue | 2 +- src/components/status_body/status_body.scss | 5 +- src/components/text.style.js | 38 ++- src/components/underlay.style.js | 3 +- src/panel.scss | 11 - src/services/color_convert/color_convert.js | 3 +- src/services/style_setter/style_setter.js | 19 +- src/services/theme_data/pleromafe.t3.js | 16 +- .../theme_data/theme_data_3.service.js | 306 +++++++++++++----- .../services/theme_data/theme_data3.spec.js | 2 +- 17 files changed, 354 insertions(+), 137 deletions(-) create mode 100644 src/components/link.style.js diff --git a/src/App.scss b/src/App.scss index ef68ac50b..8e9f3171a 100644 --- a/src/App.scss +++ b/src/App.scss @@ -24,8 +24,7 @@ body { font-family: sans-serif; font-family: var(--interfaceFont, sans-serif); margin: 0; - color: $fallback--text; - color: var(--text, $fallback--text); + color: var(--text); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; overscroll-behavior-y: none; @@ -111,8 +110,7 @@ body { a { text-decoration: none; - color: $fallback--link; - color: var(--link, $fallback--link); + color: var(--link); } h4 { @@ -128,8 +126,7 @@ h4 { i[class*="icon-"], .svg-inline--fa, .iconLetter { - color: $fallback--icon; - color: var(--icon, $fallback--icon); + color: var(--icon); } .button-unstyled:hover, @@ -763,17 +760,11 @@ option { } .faint { - color: $fallback--faint; - color: var(--faint, $fallback--faint); -} + --text: var(--textFaint); + --textGreentext: var(--textGreentextFaint); + --link: var(--linkFaint); -.faint-link { - color: $fallback--faint; - color: var(--faint, $fallback--faint); - - &:hover { - text-decoration: underline; - } + color: var(--text); } .visibility-notice { @@ -816,6 +807,11 @@ option { opacity: 0.25; } +.timeago { + --link: var(--text); + --linkFaint: var(--textFaint); +} + .login-hint { text-align: center; diff --git a/src/components/icon.style.js b/src/components/icon.style.js index 732cf16f0..adc72fc59 100644 --- a/src/components/icon.style.js +++ b/src/components/icon.style.js @@ -1,4 +1,15 @@ export default { name: 'Icon', - selector: '.icon' + virtual: true, + selector: '.svg-inline--fa', + defaultRules: [ + { + component: 'Icon', + directives: { + textColor: '--text', + textOpacity: 0.5, + textOpacityMode: 'mixrgb' + } + } + ] } diff --git a/src/components/link.style.js b/src/components/link.style.js new file mode 100644 index 000000000..0128fd91a --- /dev/null +++ b/src/components/link.style.js @@ -0,0 +1,25 @@ +export default { + name: 'Link', + selector: 'a', + virtual: true, + states: { + faint: '.faint' + }, + defaultRules: [ + { + component: 'Link', + directives: { + textColor: '--link' + } + }, + { + component: 'Link', + state: ['faint'], + directives: { + textColor: '--link', + textOpacity: 0.5, + textOpacityMode: 'fake' + } + } + ] +} diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue index a8eceab0b..5c425200b 100644 --- a/src/components/notification/notification.vue +++ b/src/components/notification/notification.vue @@ -155,7 +155,7 @@ .button-default { flex-shrink: 0; diff --git a/src/services/color_convert/color_convert.js b/src/services/color_convert/color_convert.js index 23e4ca611..d92bbbe05 100644 --- a/src/services/color_convert/color_convert.js +++ b/src/services/color_convert/color_convert.js @@ -173,7 +173,7 @@ export const mixrgb = (a, b) => { * @returns {String} CSS rgba() color */ export const rgba2css = function (rgba) { - return `rgba(${Math.floor(rgba.r)}, ${Math.floor(rgba.g)}, ${Math.floor(rgba.b)}, ${rgba.a})` + return `rgba(${Math.floor(rgba.r)}, ${Math.floor(rgba.g)}, ${Math.floor(rgba.b)}, ${rgba.a ?? 1})` } /** @@ -188,7 +188,6 @@ export const rgba2css = function (rgba) { */ export const getTextColor = function (bg, text, preserve) { const contrast = getContrastRatio(bg, text) - console.log(contrast) if (contrast < 4.5) { const base = typeof text.a !== 'undefined' ? { a: text.a } : {} diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js index 43fe3c730..ba98c4d96 100644 --- a/src/services/style_setter/style_setter.js +++ b/src/services/style_setter/style_setter.js @@ -1,10 +1,15 @@ import { convert } from 'chromatism' import { rgb2hex, hex2rgb, rgba2css, getCssColor, relativeLuminance } from '../color_convert/color_convert.js' import { getColors, computeDynamicColor, getOpacitySlot } from '../theme_data/theme_data.service.js' +import { init } from '../theme_data/theme_data_3.service.js' +import { + sampleRules +} from 'src/services/theme_data/pleromafe.t3.js' import { defaultState } from '../../modules/config.js' export const applyTheme = (input) => { - const { rules } = generatePreset(input) + const { rules, t3b } = generatePreset(input) + const themes3 = init(sampleRules, t3b) const head = document.head const body = document.body body.classList.add('hidden') @@ -18,6 +23,10 @@ export const applyTheme = (input) => { styleSheet.insertRule(`:root { ${rules.colors} }`, 'index-max') styleSheet.insertRule(`:root { ${rules.shadows} }`, 'index-max') styleSheet.insertRule(`:root { ${rules.fonts} }`, 'index-max') + themes3.css.forEach(rule => { + console.log(rule) + styleSheet.insertRule(rule, 'index-max') + }) body.classList.remove('hidden') } @@ -326,7 +335,7 @@ export const generateShadows = (input, colors) => { } } -export const composePreset = (colors, radii, shadows, fonts) => { +export const composePreset = (colors, radii, shadows, fonts, t3b) => { return { rules: { ...shadows.rules, @@ -339,7 +348,8 @@ export const composePreset = (colors, radii, shadows, fonts) => { ...colors.theme, ...radii.theme, ...fonts.theme - } + }, + t3b } } @@ -349,7 +359,8 @@ export const generatePreset = (input) => { colors, generateRadii(input), generateShadows(input, colors.theme.colors, colors.mod), - generateFonts(input) + generateFonts(input), + colors.theme.colors ) } diff --git a/src/services/theme_data/pleromafe.t3.js b/src/services/theme_data/pleromafe.t3.js index 8d8e19cd3..9fadf0eea 100644 --- a/src/services/theme_data/pleromafe.t3.js +++ b/src/services/theme_data/pleromafe.t3.js @@ -11,30 +11,30 @@ export const sampleRules = [ { component: 'Panel', directives: { - background: '#FFFFFF', - opacity: 0.9 + background: '--fg' + // opacity: 0.9 } }, { component: 'PanelHeader', directives: { - background: '#000000', - opacity: 0.9 + background: '--fg' + // opacity: 0.9 } }, { component: 'Button', directives: { - background: '#000000', - opacity: 0.8 + background: '--fg' + // opacity: 0.8 } }, { component: 'Button', state: ['hover'], directives: { - background: '#FF00FF', - opacity: 0.9 + background: '#FFFFFF' + // opacity: 0.9 } } ] diff --git a/src/services/theme_data/theme_data_3.service.js b/src/services/theme_data/theme_data_3.service.js index 2ef5e17fd..c9468d07b 100644 --- a/src/services/theme_data/theme_data_3.service.js +++ b/src/services/theme_data/theme_data_3.service.js @@ -1,11 +1,12 @@ import { convert } from 'chromatism' -import { alphaBlend, getTextColor, rgba2css } from '../color_convert/color_convert.js' +import { alphaBlend, getTextColor, rgba2css, mixrgb } from '../color_convert/color_convert.js' import Underlay from 'src/components/underlay.style.js' import Panel from 'src/components/panel.style.js' import PanelHeader from 'src/components/panel_header.style.js' import Button from 'src/components/button.style.js' import Text from 'src/components/text.style.js' +import Link from 'src/components/link.style.js' import Icon from 'src/components/icon.style.js' const root = Underlay @@ -15,6 +16,7 @@ const components = { PanelHeader, Button, Text, + Link, Icon } @@ -35,9 +37,9 @@ export const getAllPossibleCombinations = (array) => { return combos.reduce((acc, x) => [...acc, ...x], []) } -export const ruleToSelector = (rule) => { +export const ruleToSelector = (rule, isParent) => { const component = components[rule.component] - const { states, variants, selector } = component + const { states, variants, selector, outOfTreeSelector } = component const applicableStates = ((rule.state || []).filter(x => x !== 'normal')).map(state => states[state]) @@ -47,56 +49,104 @@ export const ruleToSelector = (rule) => { applicableVariant = variants[applicableVariantName] } - const selectors = [selector, applicableVariant, ...applicableStates] + let realSelector + if (isParent) { + realSelector = selector + } else { + if (outOfTreeSelector) realSelector = outOfTreeSelector + else realSelector = selector + } + + const selectors = [realSelector, applicableVariant, ...applicableStates] .toSorted((a, b) => { if (a.startsWith(':')) return 1 - else return -1 + if (!a.startsWith('.')) return -1 + else return 0 }) .join('') if (rule.parent) { - return ruleToSelector(rule.parent) + ' ' + selectors + return ruleToSelector(rule.parent, true) + ' ' + selectors } return selectors } -export const init = (ruleset) => { +export const init = (extraRuleset, palette) => { const rootName = root.name const rules = [] const rulesByComponent = {} + const ruleset = [ + ...Object.values(components).map(c => c.defaultRules || []).reduce((acc, arr) => [...acc, ...arr], []), + ...extraRuleset + ] + const addRule = (rule) => { rules.push(rule) rulesByComponent[rule.component] = rulesByComponent[rule.component] || [] rulesByComponent[rule.component].push(rule) } - const findRules = (combination) => rule => { - if (combination.component !== rule.component) return false - if (Object.prototype.hasOwnProperty.call(rule, 'variant')) { - if (combination.variant !== rule.variant) return false - } else { - if (combination.variant !== 'normal') return false - } + const findRules = (combination, parent) => rule => { + // inexact search + const doesCombinationMatch = () => { + if (combination.component !== rule.component) return false + if (Object.prototype.hasOwnProperty.call(rule, 'variant')) { + if (combination.variant !== rule.variant) return false + } else { + if (combination.variant !== 'normal') return false + } - if (Object.prototype.hasOwnProperty.call(rule, 'state')) { - const ruleStatesSet = new Set(['normal', ...(rule.state || [])]) - const combinationSet = new Set(['normal', ...combination.state]) - const setsAreEqual = combination.state.every(state => ruleStatesSet.has(state)) && - [...ruleStatesSet].every(state => combinationSet.has(state)) - return setsAreEqual - } else { - if (combination.state.length !== 1 || combination.state[0] !== 'normal') return false - return true + if (Object.prototype.hasOwnProperty.call(rule, 'state')) { + const ruleStatesSet = new Set(['normal', ...(rule.state || [])]) + const combinationSet = new Set(['normal', ...combination.state]) + const setsAreEqual = combination.state.every(state => ruleStatesSet.has(state)) && + [...ruleStatesSet].every(state => combinationSet.has(state)) + return setsAreEqual + } else { + if (combination.state.length !== 1 || combination.state[0] !== 'normal') return false + return true + } } + const combinationMatches = doesCombinationMatch() + if (!parent || !combinationMatches) return combinationMatches + + // exact search + + // unroll parents into array + const unroll = (item) => { + const out = [] + let currentParent = item.parent + while (currentParent) { + const { parent: newParent, ...rest } = currentParent + out.push(rest) + currentParent = newParent + } + return out + } + const { parent: _, ...rest } = parent + const pathSearch = [rest, ...unroll(parent)] + const pathRule = unroll(rule) + if (pathSearch.length !== pathRule.length) return false + const pathsMatch = pathSearch.every((searchRule, i) => { + const existingRule = pathRule[i] + if (existingRule.component !== searchRule.component) return false + if (existingRule.variant !== searchRule.variant) return false + const existingRuleStatesSet = new Set(['normal', ...(existingRule.state || [])]) + const searchStatesSet = new Set(['normal', ...(searchRule.state || [])]) + const setsAreEqual = existingRule.state.every(state => searchStatesSet.has(state)) && + [...searchStatesSet].every(state => existingRuleStatesSet.has(state)) + return setsAreEqual + }) + return pathsMatch } const findLowerLevelRule = (parent, filter = () => true) => { let lowerLevelComponent = null let currentParent = parent while (currentParent) { - const rulesParent = ruleset.filter(findRules(currentParent, true)) - rulesParent > 1 && console.log('OOPS') + const rulesParent = ruleset.filter(findRules(currentParent)) + rulesParent > 1 && console.warn('OOPS') lowerLevelComponent = rulesParent[rulesParent.length - 1] currentParent = currentParent.parent if (lowerLevelComponent && filter(lowerLevelComponent)) currentParent = null @@ -104,6 +154,35 @@ export const init = (ruleset) => { return filter(lowerLevelComponent) ? lowerLevelComponent : null } + const findColor = (color) => { + if (typeof color === 'string' && color.startsWith('--')) { + const name = color.substring(2) + return palette[name] + } + return color + } + + const getTextColorAlpha = (rule, lowerRule, value) => { + const opacity = rule.directives.textOpacity + const textColor = convert(findColor(value)).rgb + if (opacity === null || opacity === undefined || opacity >= 1) { + return convert(textColor).hex + } + const backgroundColor = convert(lowerRule.cache.background).rgb + if (opacity === 0) { + return convert(backgroundColor).hex + } + const opacityMode = rule.directives.textOpacityMode + switch (opacityMode) { + case 'fake': + return convert(alphaBlend(textColor, opacity, backgroundColor)).hex + case 'mixrgb': + return convert(mixrgb(backgroundColor, textColor)).hex + default: + return rgba2css({ a: opacity, ...textColor }) + } + } + const processInnerComponent = (component, parent) => { const { validInnerComponents = [], @@ -124,79 +203,156 @@ export const init = (ruleset) => { const VIRTUAL_COMPONENTS = new Set(['Text', 'Link', 'Icon']) stateVariantCombination.forEach(combination => { - const existingRules = ruleset.filter(findRules({ component: component.name, ...combination })) - const lastRule = existingRules[existingRules.length - 1] + let needRuleAdd = false - if (existingRules.length !== 0) { - const { directives } = lastRule - const rgb = convert(directives.background).rgb + if (VIRTUAL_COMPONENTS.has(component.name)) { + const selector = component.name + ruleToSelector({ component: component.name, ...combination }) + const virtualName = [ + '--', + component.name.toLowerCase(), + combination.variant === 'normal' + ? '' + : combination.variant[0].toUpperCase() + combination.variant.slice(1).toLowerCase(), + ...combination.state.filter(x => x !== 'normal').toSorted().map(state => state[0].toUpperCase() + state.slice(1).toLowerCase()) + ].join('') - // TODO: DEFAULT TEXT COLOR - const bg = findLowerLevelRule(parent)?.cache.background || convert('#FFFFFF').rgb + const lowerLevel = findLowerLevelRule(parent, (r) => { + if (components[r.component].validInnerComponents.indexOf(component.name) < 0) return false + if (r.cache.background === undefined) return false + if (r.cache.textDefined) { + return !r.cache.textDefined[selector] + } + return true + }) - if (!lastRule.cache?.background) { - const blend = directives.opacity < 1 ? alphaBlend(rgb, directives.opacity, bg) : rgb - lastRule.cache = lastRule.cache || {} - lastRule.cache.background = blend + if (!lowerLevel) return - addRule(lastRule) + let inheritedTextColorRule + const inheritedTextColorRules = findLowerLevelRule(parent, (r) => { + return r.cache?.textDefined?.[selector] + }) + + if (!inheritedTextColorRule) { + const generalTextColorRules = ruleset.filter(findRules({ component: component.name, ...combination }, null, true)) + inheritedTextColorRule = generalTextColorRules[generalTextColorRules.length - 1] + } else { + inheritedTextColorRule = inheritedTextColorRules[inheritedTextColorRules.length - 1] } - } else { - if (VIRTUAL_COMPONENTS.has(component.name)) { - const selector = component.name + ruleToSelector({ component: component.name, ...combination }) - const lowerLevel = findLowerLevelRule(parent, (r) => { - if (components[r.component].validInnerComponents.indexOf(component.name) < 0) return false - if (r.cache?.background === undefined) return false - if (r.cache.textDefined) { - return !r.cache.textDefined[selector] + let inheritedTextColor + let inheritedTextOpacity = {} + if (inheritedTextColorRule) { + inheritedTextColor = findColor(inheritedTextColorRule.directives.textColor) + // also inherit opacity settings + const { textOpacity, textOpacityMode } = inheritedTextColorRule.directives + inheritedTextOpacity = { textOpacity, textOpacityMode } + } else { + // Emergency fallback + inheritedTextColor = '#000000' + } + + const textColor = getTextColor( + convert(lowerLevel.cache.background).rgb, + convert(inheritedTextColor).rgb, + component.name === 'Link' // make it configurable? + ) + + lowerLevel.cache.textDefined = lowerLevel.cache.textDefined || {} + lowerLevel.cache.textDefined[selector] = textColor + lowerLevel.virtualDirectives = lowerLevel.virtualDirectives || {} + lowerLevel.virtualDirectives[virtualName] = getTextColorAlpha(inheritedTextColorRule, lowerLevel, textColor) + + const directives = { + textColor, + ...inheritedTextOpacity + } + + // Debug: lets you see what it think background color should be + directives.background = convert(lowerLevel.cache.background).hex + + addRule({ + parent, + virtual: true, + component: component.name, + ...combination, + cache: { background: lowerLevel.cache.background }, + directives + }) + } else { + const existingGlobalRules = ruleset.filter(findRules({ component: component.name, ...combination }, null)) + const existingRules = ruleset.filter(findRules({ component: component.name, ...combination }, parent)) + + // Global (general) rules + if (existingGlobalRules.length !== 0) { + const lastRule = existingGlobalRules[existingGlobalRules.length - 1] + const { directives } = lastRule + lastRule.cache = lastRule.cache || {} + + if (directives.background) { + const rgb = convert(findColor(directives.background)).rgb + + // TODO: DEFAULT TEXT COLOR + const bg = findLowerLevelRule(parent)?.cache.background || convert('#FFFFFF').rgb + + if (!lastRule.cache.background) { + const blend = directives.opacity < 1 ? alphaBlend(rgb, directives.opacity, bg) : rgb + lastRule.cache.background = blend + + needRuleAdd = true } - return true - }) - if (!lowerLevel) return - lowerLevel.cache.textDefined = lowerLevel.cache.textDefined || {} - lowerLevel.cache.textDefined[selector] = true - addRule({ - parent, - component: component.name, - ...combination, - directives: { - // TODO: DEFAULT TEXT COLOR - textColor: getTextColor(convert(lowerLevel.cache.background).rgb, convert('#FFFFFF').rgb, component.name === 'Link'), - // Debug: lets you see what it think background color should be - background: convert(lowerLevel.cache.background).hex - } - }) + } + + if (needRuleAdd) { + addRule(lastRule) + } + } + + if (existingRules.length !== 0) { + console.warn('MORE EXISTING RULES', existingRules) } } - innerComponents.forEach(innerComponent => processInnerComponent(innerComponent, { parent, component: name, ...combination })) }) } processInnerComponent(components[rootName]) - // console.info(rules.map(x => [ - // (parent?.component || 'root') + ' -> ' + x.component, - // // 'Cached background:' + convert(bg).hex, - // // 'Color: ' + convert(x.directives.background).hex + ' A:' + x.directives.opacity, - // JSON.stringify(x.directives) - // // '=> Blend: ' + convert(x.cache.background).hex - // ].join(' '))) - return { raw: rules, css: rules.map(rule => { - const header = ruleToSelector(rule) + ' {' + if (rule.virtual) return '' + + let selector = ruleToSelector(rule).replace(/\/\*.*\*\//g, '') + if (!selector) { + selector = 'body' + } + const header = selector + ' {' const footer = '}' + + const virtualDirectives = Object.entries(rule.virtualDirectives || {}).map(([k, v]) => { + return ' ' + k + ': ' + v + }).join(';\n') + const directives = Object.entries(rule.directives).map(([k, v]) => { switch (k) { - case 'background': return 'background-color: ' + rgba2css({ ...convert(v).rgb, a: rule.directives.opacity ?? 1 }) - case 'textColor': return 'color: ' + rgba2css({ ...convert(v).rgb, a: rule.directives.opacity ?? 1 }) + case 'background': { + return 'background-color: ' + rgba2css({ ...convert(findColor(v)).rgb, a: rule.directives.opacity ?? 1 }) + } + case 'textColor': { + return 'color: ' + v + } default: return '' } }).filter(x => x).map(x => ' ' + x).join(';\n') - return [header, directives, footer].join('\n') - }) + + return [ + header, + directives + ';', + ' color: var(--text);', + '', + virtualDirectives, + footer + ].join('\n') + }).filter(x => x) } } diff --git a/test/unit/specs/services/theme_data/theme_data3.spec.js b/test/unit/specs/services/theme_data/theme_data3.spec.js index 915bb5ce1..0d5870cc4 100644 --- a/test/unit/specs/services/theme_data/theme_data3.spec.js +++ b/test/unit/specs/services/theme_data/theme_data3.spec.js @@ -17,7 +17,7 @@ describe.only('Theme Data 3', () => { describe('init', () => { it('test simple case', () => { - const out = init(sampleRules) + const out = init(sampleRules, palette) // console.log(JSON.stringify(out, null, 2)) console.log('\n' + out.css.join('\n') + '\n') }) From d4795d2e3c363065319a978fd2d9237fb62f2fe6 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 1 Feb 2024 01:27:30 +0200 Subject: [PATCH 019/678] moved default rules to component style.js files, added some basic text inheritance --- src/components/button.style.js | 15 ++++ src/components/link.style.js | 1 - src/components/panel.style.js | 8 +++ src/components/panel_header.style.js | 9 +++ src/components/text.style.js | 10 --- src/components/underlay.style.js | 11 +++ src/services/theme_data/pleromafe.t3.js | 38 ---------- .../theme_data/theme_data_3.service.js | 71 ++++++++++++------- 8 files changed, 90 insertions(+), 73 deletions(-) diff --git a/src/components/button.style.js b/src/components/button.style.js index 51f781e14..49147c8d5 100644 --- a/src/components/button.style.js +++ b/src/components/button.style.js @@ -15,5 +15,20 @@ export default { validInnerComponents: [ 'Text', 'Icon' + ], + defaultRules: [ + { + component: 'Button', + directives: { + background: '--fg' + } + }, + { + component: 'Button', + state: ['hover'], + directives: { + background: '#FFFFFF' + } + } ] } diff --git a/src/components/link.style.js b/src/components/link.style.js index 0128fd91a..d13cef338 100644 --- a/src/components/link.style.js +++ b/src/components/link.style.js @@ -16,7 +16,6 @@ export default { component: 'Link', state: ['faint'], directives: { - textColor: '--link', textOpacity: 0.5, textOpacityMode: 'fake' } diff --git a/src/components/panel.style.js b/src/components/panel.style.js index 870f5099a..a9de7e56d 100644 --- a/src/components/panel.style.js +++ b/src/components/panel.style.js @@ -7,5 +7,13 @@ export default { 'Icon', 'Button', 'PanelHeader' + ], + defaultRules: [ + { + component: 'Panel', + directives: { + background: '--fg' + } + } ] } diff --git a/src/components/panel_header.style.js b/src/components/panel_header.style.js index e83e1f15c..01f8a67f8 100644 --- a/src/components/panel_header.style.js +++ b/src/components/panel_header.style.js @@ -6,5 +6,14 @@ export default { 'Link', 'Icon', 'Button' + ], + defaultRules: [ + { + component: 'PanelHeader', + directives: { + background: '--fg' + // opacity: 0.9 + } + } ] } diff --git a/src/components/text.style.js b/src/components/text.style.js index e52b6f68d..18472032d 100644 --- a/src/components/text.style.js +++ b/src/components/text.style.js @@ -19,7 +19,6 @@ export default { component: 'Text', state: ['faint'], directives: { - textColor: '--text', textOpacity: 0.5 } }, @@ -29,15 +28,6 @@ export default { directives: { textColor: '--cGreen' } - }, - { - component: 'Text', - variant: 'greentext', - state: ['faint'], - directives: { - textColor: '--cGreen', - textOpacity: 0.5 - } } ] } diff --git a/src/components/underlay.style.js b/src/components/underlay.style.js index c35fbada9..380ea26ef 100644 --- a/src/components/underlay.style.js +++ b/src/components/underlay.style.js @@ -4,5 +4,16 @@ export default { outOfTreeSelector: '.underlay', validInnerComponents: [ 'Panel' + ], + defaultRules: [ + { + component: 'Underlay', + // variant: 'normal', + // state: 'normal' + directives: { + background: '#000000', + opacity: 0.2 + } + } ] } diff --git a/src/services/theme_data/pleromafe.t3.js b/src/services/theme_data/pleromafe.t3.js index 9fadf0eea..db612a5b5 100644 --- a/src/services/theme_data/pleromafe.t3.js +++ b/src/services/theme_data/pleromafe.t3.js @@ -1,40 +1,2 @@ export const sampleRules = [ - { - component: 'Underlay', - // variant: 'normal', - // state: 'normal' - directives: { - background: '#000000', - opacity: 0.2 - } - }, - { - component: 'Panel', - directives: { - background: '--fg' - // opacity: 0.9 - } - }, - { - component: 'PanelHeader', - directives: { - background: '--fg' - // opacity: 0.9 - } - }, - { - component: 'Button', - directives: { - background: '--fg' - // opacity: 0.8 - } - }, - { - component: 'Button', - state: ['hover'], - directives: { - background: '#FFFFFF' - // opacity: 0.9 - } - } ] diff --git a/src/services/theme_data/theme_data_3.service.js b/src/services/theme_data/theme_data_3.service.js index c9468d07b..b7679021e 100644 --- a/src/services/theme_data/theme_data_3.service.js +++ b/src/services/theme_data/theme_data_3.service.js @@ -1,5 +1,6 @@ -import { convert } from 'chromatism' -import { alphaBlend, getTextColor, rgba2css, mixrgb } from '../color_convert/color_convert.js' +import { convert, brightness } from 'chromatism' +import merge from 'lodash.merge' +import { alphaBlend, getTextColor, rgba2css, mixrgb, relativeLuminance } from '../color_convert/color_convert.js' import Underlay from 'src/components/underlay.style.js' import Panel from 'src/components/panel.style.js' @@ -87,27 +88,35 @@ export const init = (extraRuleset, palette) => { rulesByComponent[rule.component].push(rule) } - const findRules = (combination, parent) => rule => { + const findRules = (searchCombination, parent) => rule => { // inexact search const doesCombinationMatch = () => { - if (combination.component !== rule.component) return false - if (Object.prototype.hasOwnProperty.call(rule, 'variant')) { - if (combination.variant !== rule.variant) return false - } else { - if (combination.variant !== 'normal') return false + if (searchCombination.component !== rule.component) return false + const ruleVariant = Object.prototype.hasOwnProperty.call(rule, 'variant') ? rule.variant : 'normal' + + if (ruleVariant !== 'normal') { + if (searchCombination.variant !== rule.variant) return false } - if (Object.prototype.hasOwnProperty.call(rule, 'state')) { - const ruleStatesSet = new Set(['normal', ...(rule.state || [])]) - const combinationSet = new Set(['normal', ...combination.state]) - const setsAreEqual = combination.state.every(state => ruleStatesSet.has(state)) && + const ruleHasStateDefined = Object.prototype.hasOwnProperty.call(rule, 'state') + let ruleStateSet + if (ruleHasStateDefined) { + ruleStateSet = new Set(['normal', ...rule.state]) + } else { + ruleStateSet = new Set(['normal']) + } + + if (ruleStateSet.size > 1) { + const ruleStatesSet = ruleStateSet + const combinationSet = new Set(['normal', ...searchCombination.state]) + const setsAreEqual = searchCombination.state.every(state => ruleStatesSet.has(state)) && [...ruleStatesSet].every(state => combinationSet.has(state)) return setsAreEqual } else { - if (combination.state.length !== 1 || combination.state[0] !== 'normal') return false return true } } + const combinationMatches = doesCombinationMatch() if (!parent || !combinationMatches) return combinationMatches @@ -154,21 +163,31 @@ export const init = (extraRuleset, palette) => { return filter(lowerLevelComponent) ? lowerLevelComponent : null } - const findColor = (color) => { - if (typeof color === 'string' && color.startsWith('--')) { - const name = color.substring(2) - return palette[name] + const findColor = (color, background) => { + if (typeof color !== 'string' || !color.startsWith('--')) return color + let targetColor = null + // Color references other color + const [variable, modifier] = color.split(/,/g).map(str => str.trim()) + const variableSlot = variable.substring(2) + targetColor = palette[variableSlot] + + if (modifier) { + const effectiveBackground = background ?? targetColor + const isLightOnDark = relativeLuminance(convert(effectiveBackground).rgb) < 0.5 + const mod = isLightOnDark ? 1 : -1 + targetColor = brightness(Number.parseFloat(modifier) * mod, targetColor).rgb } - return color + + return targetColor } const getTextColorAlpha = (rule, lowerRule, value) => { const opacity = rule.directives.textOpacity - const textColor = convert(findColor(value)).rgb + const backgroundColor = convert(lowerRule.cache.background).rgb + const textColor = convert(findColor(value, backgroundColor)).rgb if (opacity === null || opacity === undefined || opacity >= 1) { return convert(textColor).hex } - const backgroundColor = convert(lowerRule.cache.background).rgb if (opacity === 0) { return convert(backgroundColor).hex } @@ -217,6 +236,7 @@ export const init = (extraRuleset, palette) => { ].join('') const lowerLevel = findLowerLevelRule(parent, (r) => { + if (!r) return false if (components[r.component].validInnerComponents.indexOf(component.name) < 0) return false if (r.cache.background === undefined) return false if (r.cache.textDefined) { @@ -234,15 +254,15 @@ export const init = (extraRuleset, palette) => { if (!inheritedTextColorRule) { const generalTextColorRules = ruleset.filter(findRules({ component: component.name, ...combination }, null, true)) - inheritedTextColorRule = generalTextColorRules[generalTextColorRules.length - 1] + inheritedTextColorRule = generalTextColorRules.reduce((acc, rule) => merge(acc, rule), {}) } else { - inheritedTextColorRule = inheritedTextColorRules[inheritedTextColorRules.length - 1] + inheritedTextColorRule = inheritedTextColorRules.reduce((acc, rule) => merge(acc, rule), {}) } let inheritedTextColor let inheritedTextOpacity = {} if (inheritedTextColorRule) { - inheritedTextColor = findColor(inheritedTextColorRule.directives.textColor) + inheritedTextColor = findColor(inheritedTextColorRule.directives.textColor, convert(lowerLevel.cache.background).rgb) // also inherit opacity settings const { textOpacity, textOpacityMode } = inheritedTextColorRule.directives inheritedTextOpacity = { textOpacity, textOpacityMode } @@ -284,8 +304,11 @@ export const init = (extraRuleset, palette) => { // Global (general) rules if (existingGlobalRules.length !== 0) { + const totalRule = existingGlobalRules.reduce((acc, rule) => merge(acc, rule), {}) + const { directives } = totalRule + + // last rule is used as a cache const lastRule = existingGlobalRules[existingGlobalRules.length - 1] - const { directives } = lastRule lastRule.cache = lastRule.cache || {} if (directives.background) { From c34590c43983711152a843a2e36b991a5fe3bcdf Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 7 Feb 2024 15:53:34 +0200 Subject: [PATCH 020/678] update, should inherit stuff properly now. --- src/App.scss | 44 +- src/components/button.style.js | 66 ++- src/components/dropdown_menu.style.js | 19 + src/components/input.style.js | 60 ++ src/components/panel.style.js | 7 +- src/components/panel_header.style.js | 1 - src/components/popover.style.js | 20 + src/components/root.style.js | 17 + src/components/top_bar.style.js | 18 + src/components/underlay.style.js | 2 +- src/panel.scss | 9 +- src/services/style_setter/style_setter.js | 4 +- src/services/theme_data/theme2_keys.js | 176 ++++++ src/services/theme_data/theme2_to_theme3.js | 58 ++ .../theme_data/theme_data_3.service.js | 530 +++++++++++------- top_bar.style.js | 9 + 16 files changed, 804 insertions(+), 236 deletions(-) create mode 100644 src/components/dropdown_menu.style.js create mode 100644 src/components/input.style.js create mode 100644 src/components/popover.style.js create mode 100644 src/components/root.style.js create mode 100644 src/components/top_bar.style.js create mode 100644 src/services/theme_data/theme2_keys.js create mode 100644 src/services/theme_data/theme2_to_theme3.js create mode 100644 top_bar.style.js diff --git a/src/App.scss b/src/App.scss index 8e9f3171a..a8eecaf54 100644 --- a/src/App.scss +++ b/src/App.scss @@ -371,8 +371,7 @@ nav { border-radius: $fallback--btnRadius; border-radius: var(--btnRadius, $fallback--btnRadius); cursor: pointer; - box-shadow: $fallback--buttonShadow; - box-shadow: var(--buttonShadow); + box-shadow: var(--shadow); font-size: 1em; font-family: sans-serif; font-family: var(--interfaceFont, sans-serif); @@ -383,25 +382,14 @@ nav { i[class*="icon-"], .svg-inline--fa { - color: $fallback--text; - color: var(--btnText, $fallback--text); + color: var(--icon); } &::-moz-focus-inner { border: none; } - &:hover { - box-shadow: 0 0 4px rgb(255 255 255 / 30%); - box-shadow: var(--buttonHoverShadow); - } - &:active { - box-shadow: - 0 0 4px 0 rgb(255 255 255 / 30%), - 0 1px 0 0 rgb(0 0 0 / 20%) inset, - 0 -1px 0 0 rgb(255 255 255 / 20%) inset; - box-shadow: var(--buttonPressedShadow); color: $fallback--text; color: var(--btnPressedText, $fallback--text); background-color: $fallback--fg; @@ -487,7 +475,12 @@ nav { } input, -textarea, +textarea { + border: none; + display: inline-block; + outline: none; +} + .input { &.unstyled { border-radius: 0; @@ -501,15 +494,7 @@ textarea, border: none; border-radius: $fallback--inputRadius; border-radius: var(--inputRadius, $fallback--inputRadius); - box-shadow: - 0 1px 0 0 rgb(0 0 0 / 20%) inset, - 0 -1px 0 0 rgb(255 255 255 / 20%) inset, - 0 0 2px 0 rgb(0 0 0 / 100%) inset; - box-shadow: var(--inputShadow); - background-color: $fallback--fg; - background-color: var(--input, $fallback--fg); - color: $fallback--lightText; - color: var(--inputText, $fallback--lightText); + box-shadow: var(--shadow); font-family: sans-serif; font-family: var(--inputFont, sans-serif); font-size: 1em; @@ -561,11 +546,9 @@ textarea, width: 1.1em; height: 1.1em; border-radius: 100%; // Radio buttons should always be circle - box-shadow: 0 0 2px black inset; - box-shadow: var(--inputShadow); + background-color: var(--background); + box-shadow: var(--shadow); margin-right: 0.5em; - background-color: $fallback--fg; - background-color: var(--input, $fallback--fg); vertical-align: top; text-align: center; line-height: 1.1; @@ -578,8 +561,9 @@ textarea, &[type="checkbox"] { &:checked + label::before { - color: $fallback--text; - color: var(--inputText, $fallback--text); + color: var(--text); + background-color: var(--background); + box-shadow: var(--shadow); } &:disabled { diff --git a/src/components/button.style.js b/src/components/button.style.js index 49147c8d5..2f74d127a 100644 --- a/src/components/button.style.js +++ b/src/components/button.style.js @@ -1,11 +1,34 @@ +const border = (top, shadow) => ({ + x: 0, + y: top ? 1 : -1, + blur: 0, + spread: 0, + color: shadow ? '#000000' : '#FFFFFF', + alpha: 0.2, + inset: true +}) + +const buttonInsetFakeBorders = [border(true, false), border(false, true)] +const inputInsetFakeBorders = [border(true, true), border(false, false)] + +const hoverGlow = { + x: 0, + y: 0, + blur: 4, + spread: 0, + color: '--text', + alpha: 1 +} + export default { name: 'Button', - selector: '.btn', + selector: '.button-default', states: { disabled: ':disabled', toggled: '.toggled', pressed: ':active', - hover: ':hover' + hover: ':hover', + focused: ':focus-within' }, variants: { danger: '.danger', @@ -20,14 +43,49 @@ export default { { component: 'Button', directives: { - background: '--fg' + background: '--fg', + shadow: [{ + x: 0, + y: 0, + blur: 2, + spread: 0, + color: '#000000', + alpha: 1 + }, ...buttonInsetFakeBorders] } }, { component: 'Button', state: ['hover'], directives: { - background: '#FFFFFF' + shadow: [hoverGlow, ...buttonInsetFakeBorders] + } + }, + { + component: 'Button', + state: ['hover', 'pressed'], + directives: { + background: '--accent,-24.2', + shadow: [hoverGlow, ...inputInsetFakeBorders] + } + }, + { + component: 'Button', + state: ['disabled'], + directives: { + background: '$blend(--background, 0.25, --parent)', + shadow: [...buttonInsetFakeBorders] + } + }, + { + component: 'Text', + parent: { + component: 'Button', + state: ['disabled'] + }, + directives: { + textOpacity: 0.25, + textOpacityMode: 'blend' } } ] diff --git a/src/components/dropdown_menu.style.js b/src/components/dropdown_menu.style.js new file mode 100644 index 000000000..905984e92 --- /dev/null +++ b/src/components/dropdown_menu.style.js @@ -0,0 +1,19 @@ +export default { + name: 'DropdownMenu', + selector: '.dropdown', + validInnerComponents: [ + 'Text', + 'Icon', + 'Input' + ], + states: { + hover: ':hover' + }, + defaultRules: [ + { + directives: { + background: '--fg' + } + } + ] +} diff --git a/src/components/input.style.js b/src/components/input.style.js new file mode 100644 index 000000000..48d74bfa9 --- /dev/null +++ b/src/components/input.style.js @@ -0,0 +1,60 @@ +const border = (top, shadow) => ({ + x: 0, + y: top ? 1 : -1, + blur: 0, + spread: 0, + color: shadow ? '#000000' : '#FFFFFF', + alpha: 0.2, + inset: true +}) + +const inputInsetFakeBorders = [border(true, true), border(false, false)] + +const hoverGlow = { + x: 0, + y: 0, + blur: 4, + spread: 0, + color: '--text', + alpha: 1 +} + +export default { + name: 'Input', + selector: '.input', + states: { + disabled: ':disabled', + pressed: ':active', + hover: ':hover', + focused: ':focus-within' + }, + variants: { + danger: '.danger', + unstyled: '.unstyled', + sublime: '.sublime' + }, + validInnerComponents: [ + 'Text' + ], + defaultRules: [ + { + directives: { + background: '--fg', + shadow: [{ + x: 0, + y: 0, + blur: 2, + spread: 0, + color: '#000000', + alpha: 1 + }, ...inputInsetFakeBorders] + } + }, + { + state: ['hover'], + directives: { + shadow: [hoverGlow, ...inputInsetFakeBorders] + } + } + ] +} diff --git a/src/components/panel.style.js b/src/components/panel.style.js index a9de7e56d..d93792e99 100644 --- a/src/components/panel.style.js +++ b/src/components/panel.style.js @@ -6,13 +6,14 @@ export default { 'Link', 'Icon', 'Button', - 'PanelHeader' + 'Input', + 'PanelHeader', + 'DropdownMenu' ], defaultRules: [ { - component: 'Panel', directives: { - background: '--fg' + background: '--bg' } } ] diff --git a/src/components/panel_header.style.js b/src/components/panel_header.style.js index 01f8a67f8..e18fa799d 100644 --- a/src/components/panel_header.style.js +++ b/src/components/panel_header.style.js @@ -12,7 +12,6 @@ export default { component: 'PanelHeader', directives: { background: '--fg' - // opacity: 0.9 } } ] diff --git a/src/components/popover.style.js b/src/components/popover.style.js new file mode 100644 index 000000000..415795a85 --- /dev/null +++ b/src/components/popover.style.js @@ -0,0 +1,20 @@ +export default { + name: 'Popover', + selector: '.popover', + validInnerComponents: [ + 'Text', + 'Link', + 'Icon', + 'Button', + 'Input', + 'PanelHeader', + 'DropdownMenu' + ], + defaultRules: [ + { + directives: { + background: '--fg' + } + } + ] +} diff --git a/src/components/root.style.js b/src/components/root.style.js new file mode 100644 index 000000000..b762b2ba5 --- /dev/null +++ b/src/components/root.style.js @@ -0,0 +1,17 @@ +export default { + name: 'Root', + selector: ':root', + validInnerComponents: [ + 'Underlay', + 'TopBar', + 'Popover' + ], + defaultRules: [ + { + directives: { + background: '--bg', + opacity: 0 + } + } + ] +} diff --git a/src/components/top_bar.style.js b/src/components/top_bar.style.js new file mode 100644 index 000000000..99e8efdc1 --- /dev/null +++ b/src/components/top_bar.style.js @@ -0,0 +1,18 @@ +export default { + name: 'TopBar', + selector: 'nav', + validInnerComponents: [ + 'Link', + 'Text', + 'Icon', + 'Button', + 'Input' + ], + defaultRules: [ + { + directives: { + background: '--fg' + } + } + ] +} diff --git a/src/components/underlay.style.js b/src/components/underlay.style.js index 380ea26ef..48965f20b 100644 --- a/src/components/underlay.style.js +++ b/src/components/underlay.style.js @@ -1,6 +1,6 @@ export default { name: 'Underlay', - selector: '#content', + selector: 'body', // Should be '#content' but for now this is better for testing until I have proper popovers and such... outOfTreeSelector: '.underlay', validInnerComponents: [ 'Panel' diff --git a/src/panel.scss b/src/panel.scss index d6816937b..7d5721338 100644 --- a/src/panel.scss +++ b/src/panel.scss @@ -6,6 +6,10 @@ background-color: $fallback--bg; background-color: var(--bg, $fallback--bg); + .panel-heading { + background-color: inherit; + } + &::after, & { border-radius: $fallback--panelRadius; @@ -131,12 +135,9 @@ align-items: start; // panel theme color: var(--panelText); - background-color: $fallback--bg; - background-color: var(--bg, $fallback--bg); &::after { - background-color: $fallback--fg; - background-color: var(--panel, $fallback--fg); + background-color: var(--background); z-index: -2; border-radius: $fallback--panelRadius $fallback--panelRadius 0 0; border-radius: var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius) 0 0; diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js index ba98c4d96..c09b3790f 100644 --- a/src/services/style_setter/style_setter.js +++ b/src/services/style_setter/style_setter.js @@ -20,8 +20,8 @@ export const applyTheme = (input) => { styleSheet.toString() styleSheet.insertRule(`:root { ${rules.radii} }`, 'index-max') - styleSheet.insertRule(`:root { ${rules.colors} }`, 'index-max') - styleSheet.insertRule(`:root { ${rules.shadows} }`, 'index-max') + // styleSheet.insertRule(`:root { ${rules.colors} }`, 'index-max') + // styleSheet.insertRule(`:root { ${rules.shadows} }`, 'index-max') styleSheet.insertRule(`:root { ${rules.fonts} }`, 'index-max') themes3.css.forEach(rule => { console.log(rule) diff --git a/src/services/theme_data/theme2_keys.js b/src/services/theme_data/theme2_keys.js new file mode 100644 index 000000000..09092e0c4 --- /dev/null +++ b/src/services/theme_data/theme2_keys.js @@ -0,0 +1,176 @@ +export default [ + 'bg', + 'wallpaper', + 'fg', + 'text', + 'underlay', + 'link', + 'accent', + 'faint', + 'faintLink', + 'postFaintLink', + + 'cBlue', + 'cRed', + 'cGreen', + 'cOrange', + + 'profileBg', + 'profileTint', + + 'highlight', + 'highlightLightText', + 'highlightPostLink', + 'highlightFaintText', + 'highlightFaintLink', + 'highlightPostFaintLink', + 'highlightText', + 'highlightLink', + 'highlightIcon', + + 'popover', + 'popoverLightText', + 'popoverPostLink', + 'popoverFaintText', + 'popoverFaintLink', + 'popoverPostFaintLink', + 'popoverText', + 'popoverLink', + 'popoverIcon', + + 'selectedPost', + 'selectedPostFaintText', + 'selectedPostLightText', + 'selectedPostPostLink', + 'selectedPostFaintLink', + 'selectedPostText', + 'selectedPostLink', + 'selectedPostIcon', + + 'selectedMenu', + 'selectedMenuLightText', + 'selectedMenuFaintText', + 'selectedMenuFaintLink', + 'selectedMenuText', + 'selectedMenuLink', + 'selectedMenuIcon', + + 'selectedMenuPopover', + 'selectedMenuPopoverLightText', + 'selectedMenuPopoverFaintText', + 'selectedMenuPopoverFaintLink', + 'selectedMenuPopoverText', + 'selectedMenuPopoverLink', + 'selectedMenuPopoverIcon', + + 'lightText', + + 'postLink', + + 'postGreentext', + + 'postCyantext', + + 'border', + + 'poll', + 'pollText', + + 'icon', + + // Foreground, + 'fgText', + 'fgLink', + + // Panel header, + 'panel', + 'panelText', + 'panelFaint', + 'panelLink', + + // Top bar, + 'topBar', + 'topBarLink', + + // Tabs, + 'tab', + 'tabText', + 'tabActiveText', + + // Buttons, + 'btn', + 'btnText', + 'btnPanelText', + 'btnTopBarText', + + // Buttons: pressed, + 'btnPressed', + 'btnPressedText', + 'btnPressedPanel', + 'btnPressedPanelText', + 'btnPressedTopBar', + 'btnPressedTopBarText', + + // Buttons: toggled, + 'btnToggled', + 'btnToggledText', + 'btnToggledPanelText', + 'btnToggledTopBarText', + + // Buttons: disabled, + 'btnDisabled', + 'btnDisabledText', + 'btnDisabledPanelText', + 'btnDisabledTopBarText', + + // Input fields, + 'input', + 'inputText', + 'inputPanelText', + 'inputTopbarText', + + 'alertError', + 'alertErrorText', + 'alertErrorPanelText', + + 'alertWarning', + 'alertWarningText', + 'alertWarningPanelText', + + 'alertSuccess', + 'alertSuccessText', + 'alertSuccessPanelText', + + 'alertNeutral', + 'alertNeutralText', + 'alertNeutralPanelText', + + 'alertPopupError', + 'alertPopupErrorText', + + 'alertPopupWarning', + 'alertPopupWarningText', + + 'alertPopupSuccess', + 'alertPopupSuccessText', + + 'alertPopupNeutral', + 'alertPopupNeutralText', + + 'badgeNotification', + 'badgeNotificationText', + + 'badgeNeutral', + 'badgeNeutralText', + + 'chatBg', + + 'chatMessageIncomingBg', + 'chatMessageIncomingText', + 'chatMessageIncomingLink', + 'chatMessageIncomingBorder', + 'chatMessageOutgoingBg', + 'chatMessageOutgoingText', + 'chatMessageOutgoingLink', + 'chatMessageOutgoingBorder' +] diff --git a/src/services/theme_data/theme2_to_theme3.js b/src/services/theme_data/theme2_to_theme3.js new file mode 100644 index 000000000..5ac87bebb --- /dev/null +++ b/src/services/theme_data/theme2_to_theme3.js @@ -0,0 +1,58 @@ +import allKeys from './theme2_keys' + +// keys that are meant to be used globally, i.e. what's the rest of the theme is based upon. +const basePaletteKeys = new Set([ + 'bg', + 'fg', + 'text', + 'link', + 'accent', + + 'cBlue', + 'cRed', + 'cGreen', + 'cOrange' +]) + +// Keys that are not available in editor and never meant to be edited +const hiddenKeys = new Set([ + 'profileBg', + 'profileTint' +]) + +const extendedBasePrefixes = [ + 'border', + 'icon', + 'highlight', + 'lightText', + + 'popover', + + 'panel', + 'topBar', + 'tab', + 'btn', + 'input', + 'selectedMenu', + + 'alert', + 'badge', + + 'post', + 'selectedPost', // wrong nomenclature + 'poll', + + 'chatBg', + 'chatMessageIncoming', + 'chatMessageOutgoing' +] + +const extendedBaseKeys = Object.fromEntries(extendedBasePrefixes.map(prefix => [prefix, allKeys.filter(k => k.startsWith(prefix))])) + +// Keysets that are only really used intermideately, i.e. to generate other colors +const temporary = new Set([ + 'border', + 'highlight' +]) + +const temporaryColors = {} diff --git a/src/services/theme_data/theme_data_3.service.js b/src/services/theme_data/theme_data_3.service.js index b7679021e..1228efc21 100644 --- a/src/services/theme_data/theme_data_3.service.js +++ b/src/services/theme_data/theme_data_3.service.js @@ -1,24 +1,89 @@ import { convert, brightness } from 'chromatism' import merge from 'lodash.merge' -import { alphaBlend, getTextColor, rgba2css, mixrgb, relativeLuminance } from '../color_convert/color_convert.js' +import { + alphaBlend, + getTextColor, + rgba2css, + mixrgb, + relativeLuminance +} from '../color_convert/color_convert.js' +import Root from 'src/components/root.style.js' +import TopBar from 'src/components/top_bar.style.js' import Underlay from 'src/components/underlay.style.js' +import Popover from 'src/components/popover.style.js' +import DropdownMenu from 'src/components/dropdown_menu.style.js' import Panel from 'src/components/panel.style.js' import PanelHeader from 'src/components/panel_header.style.js' import Button from 'src/components/button.style.js' +import Input from 'src/components/input.style.js' import Text from 'src/components/text.style.js' import Link from 'src/components/link.style.js' import Icon from 'src/components/icon.style.js' -const root = Underlay +export const DEFAULT_SHADOWS = { + panel: [{ + x: 1, + y: 1, + blur: 4, + spread: 0, + color: '#000000', + alpha: 0.6 + }], + topBar: [{ + x: 0, + y: 0, + blur: 4, + spread: 0, + color: '#000000', + alpha: 0.6 + }], + popup: [{ + x: 2, + y: 2, + blur: 3, + spread: 0, + color: '#000000', + alpha: 0.5 + }], + avatar: [{ + x: 0, + y: 1, + blur: 8, + spread: 0, + color: '#000000', + alpha: 0.7 + }], + avatarStatus: [], + panelHeader: [] +} + const components = { - Underlay, - Panel, - PanelHeader, - Button, + Root, Text, Link, - Icon + Icon, + Underlay, + Popover, + DropdownMenu, + Panel, + PanelHeader, + TopBar, + Button, + Input +} + +// "Unrolls" a tree structure of item: { parent: { ...item2, parent: { ...item3, parent: {...} } }} +// into an array [item2, item3] for iterating +const unroll = (item) => { + const out = [] + let currentParent = item.parent + while (currentParent) { + const { parent: newParent, ...rest } = currentParent + out.push(rest) + currentParent = newParent + } + return out } // This gives you an array of arrays of all possible unique (i.e. order-insensitive) combinations @@ -38,7 +103,9 @@ export const getAllPossibleCombinations = (array) => { return combos.reduce((acc, x) => [...acc, ...x], []) } -export const ruleToSelector = (rule, isParent) => { +// Converts rule, parents and their criteria into a CSS (or path if ignoreOutOfTreeSelector == true) selector +export const ruleToSelector = (rule, ignoreOutOfTreeSelector, isParent) => { + if (!rule && !isParent) return null const component = components[rule.component] const { states, variants, selector, outOfTreeSelector } = component @@ -51,10 +118,12 @@ export const ruleToSelector = (rule, isParent) => { } let realSelector - if (isParent) { + if (selector === ':root') { + realSelector = '' + } else if (isParent) { realSelector = selector } else { - if (outOfTreeSelector) realSelector = outOfTreeSelector + if (outOfTreeSelector && !ignoreOutOfTreeSelector) realSelector = outOfTreeSelector else realSelector = selector } @@ -67,123 +136,133 @@ export const ruleToSelector = (rule, isParent) => { .join('') if (rule.parent) { - return ruleToSelector(rule.parent, true) + ' ' + selectors + return (ruleToSelector(rule.parent, ignoreOutOfTreeSelector, true) + ' ' + selectors).trim() } - return selectors + return selectors.trim() +} + +const combinationsMatch = (criteria, subject) => { + if (criteria.component !== subject.component) return false + + // All variants inherit from normal + const subjectVariant = Object.prototype.hasOwnProperty.call(subject, 'variant') ? subject.variant : 'normal' + if (subjectVariant !== 'normal') { + if (criteria.variant !== subject.variant) return false + } + + const subjectStatesSet = new Set(['normal', ...(subject.state || [])]) + const criteriaStatesSet = new Set(['normal', ...(criteria.state || [])]) + + // Subject states > 1 essentially means state is "normal" and therefore matches + if (subjectStatesSet.size > 1) { + const setsAreEqual = + [...criteriaStatesSet].every(state => subjectStatesSet.has(state)) && + [...subjectStatesSet].every(state => criteriaStatesSet.has(state)) + + if (!setsAreEqual) return false + } + return true +} + +const findRules = criteria => subject => { + // If we searching for "general" rules - ignore "specific" ones + if (criteria.parent === null && !!subject.parent) return false + if (!combinationsMatch(criteria, subject)) return false + + if (criteria.parent !== undefined && criteria.parent !== null) { + if (!subject.parent) return true + const pathCriteria = unroll(criteria) + const pathSubject = unroll(subject) + if (pathCriteria.length < pathSubject.length) return false + + // Search: .a .b .c + // Matches: .a .b .c; .b .c; .c; .z .a .b .c + // Does not match .a .b .c .d, .a .b .e + for (let i = 0; i < pathCriteria.length; i++) { + const criteriaParent = pathCriteria[i] + const subjectParent = pathSubject[i] + if (!subjectParent) return true + if (!combinationsMatch(criteriaParent, subjectParent)) return false + } + } + return true } export const init = (extraRuleset, palette) => { - const rootName = root.name + const cache = {} + const computed = {} + const rules = [] - const rulesByComponent = {} const ruleset = [ - ...Object.values(components).map(c => c.defaultRules || []).reduce((acc, arr) => [...acc, ...arr], []), + ...Object.values(components).map(c => c.defaultRules.map(r => ({ component: c.name, ...r })) || []).reduce((acc, arr) => [...acc, ...arr], []), ...extraRuleset ] + const virtualComponents = new Set(Object.values(components).filter(c => c.virtual).map(c => c.name)) + const addRule = (rule) => { rules.push(rule) - rulesByComponent[rule.component] = rulesByComponent[rule.component] || [] - rulesByComponent[rule.component].push(rule) } - const findRules = (searchCombination, parent) => rule => { - // inexact search - const doesCombinationMatch = () => { - if (searchCombination.component !== rule.component) return false - const ruleVariant = Object.prototype.hasOwnProperty.call(rule, 'variant') ? rule.variant : 'normal' - - if (ruleVariant !== 'normal') { - if (searchCombination.variant !== rule.variant) return false - } - - const ruleHasStateDefined = Object.prototype.hasOwnProperty.call(rule, 'state') - let ruleStateSet - if (ruleHasStateDefined) { - ruleStateSet = new Set(['normal', ...rule.state]) - } else { - ruleStateSet = new Set(['normal']) - } - - if (ruleStateSet.size > 1) { - const ruleStatesSet = ruleStateSet - const combinationSet = new Set(['normal', ...searchCombination.state]) - const setsAreEqual = searchCombination.state.every(state => ruleStatesSet.has(state)) && - [...ruleStatesSet].every(state => combinationSet.has(state)) - return setsAreEqual - } else { - return true - } - } - - const combinationMatches = doesCombinationMatch() - if (!parent || !combinationMatches) return combinationMatches - - // exact search - - // unroll parents into array - const unroll = (item) => { - const out = [] - let currentParent = item.parent - while (currentParent) { - const { parent: newParent, ...rest } = currentParent - out.push(rest) - currentParent = newParent - } - return out - } - const { parent: _, ...rest } = parent - const pathSearch = [rest, ...unroll(parent)] - const pathRule = unroll(rule) - if (pathSearch.length !== pathRule.length) return false - const pathsMatch = pathSearch.every((searchRule, i) => { - const existingRule = pathRule[i] - if (existingRule.component !== searchRule.component) return false - if (existingRule.variant !== searchRule.variant) return false - const existingRuleStatesSet = new Set(['normal', ...(existingRule.state || [])]) - const searchStatesSet = new Set(['normal', ...(searchRule.state || [])]) - const setsAreEqual = existingRule.state.every(state => searchStatesSet.has(state)) && - [...searchStatesSet].every(state => existingRuleStatesSet.has(state)) - return setsAreEqual - }) - return pathsMatch - } - - const findLowerLevelRule = (parent, filter = () => true) => { - let lowerLevelComponent = null - let currentParent = parent - while (currentParent) { - const rulesParent = ruleset.filter(findRules(currentParent)) - rulesParent > 1 && console.warn('OOPS') - lowerLevelComponent = rulesParent[rulesParent.length - 1] - currentParent = currentParent.parent - if (lowerLevelComponent && filter(lowerLevelComponent)) currentParent = null - } - return filter(lowerLevelComponent) ? lowerLevelComponent : null - } - - const findColor = (color, background) => { - if (typeof color !== 'string' || !color.startsWith('--')) return color + const findColor = (color, inheritedBackground, lowerLevelBackground) => { + if (typeof color !== 'string' || (!color.startsWith('--') && !color.startsWith('$'))) return color let targetColor = null - // Color references other color - const [variable, modifier] = color.split(/,/g).map(str => str.trim()) - const variableSlot = variable.substring(2) - targetColor = palette[variableSlot] + if (color.startsWith('--')) { + const [variable, modifier] = color.split(/,/g).map(str => str.trim()) + const variableSlot = variable.substring(2) + if (variableSlot.startsWith('parent')) { + // TODO support more than just background? + if (variableSlot === 'parent') { + targetColor = lowerLevelBackground + } + } else { + switch (variableSlot) { + case 'background': + targetColor = inheritedBackground + break + default: + targetColor = palette[variableSlot] + } + } - if (modifier) { - const effectiveBackground = background ?? targetColor - const isLightOnDark = relativeLuminance(convert(effectiveBackground).rgb) < 0.5 - const mod = isLightOnDark ? 1 : -1 - targetColor = brightness(Number.parseFloat(modifier) * mod, targetColor).rgb + if (modifier) { + const effectiveBackground = lowerLevelBackground ?? targetColor + const isLightOnDark = relativeLuminance(convert(effectiveBackground).rgb) < 0.5 + const mod = isLightOnDark ? 1 : -1 + targetColor = brightness(Number.parseFloat(modifier) * mod, targetColor).rgb + } } - + if (color.startsWith('$')) { + try { + const { funcName, argsString } = /\$(?\w+)\((?[a-zA-Z0-9-,.'"\s]*)\)/.exec(color).groups + const args = argsString.split(/,/g).map(a => a.trim()) + switch (funcName) { + case 'blend': { + if (args.length !== 3) { + throw new Error(`$blend requires 3 arguments, ${args.length} were provided`) + } + const backgroundArg = findColor(args[2], inheritedBackground, lowerLevelBackground) + const foregroundArg = findColor(args[0], inheritedBackground, lowerLevelBackground) + const amount = Number(args[1]) + targetColor = alphaBlend(backgroundArg, amount, foregroundArg) + break + } + } + } catch (e) { + console.error('Failure executing color function', e) + targetColor = '#FF00FF' + } + } + // Color references other color return targetColor } - const getTextColorAlpha = (rule, lowerRule, value) => { + const cssColorString = (color, alpha) => rgba2css({ ...convert(color).rgb, a: alpha }) + + const getTextColorAlpha = (rule, lowerColor, value) => { const opacity = rule.directives.textOpacity - const backgroundColor = convert(lowerRule.cache.background).rgb + const backgroundColor = convert(lowerColor).rgb const textColor = convert(findColor(value, backgroundColor)).rgb if (opacity === null || opacity === undefined || opacity >= 1) { return convert(textColor).hex @@ -202,6 +281,44 @@ export const init = (extraRuleset, palette) => { } } + const getCssShadow = (input, usesDropShadow) => { + if (input.length === 0) { + return 'none' + } + + return input + .filter(_ => usesDropShadow ? _.inset : _) + .map((shad) => [ + shad.x, + shad.y, + shad.blur, + shad.spread + ].map(_ => _ + 'px ').concat([ + cssColorString(findColor(shad.color), shad.alpha), + shad.inset ? 'inset' : '' + ]).join(' ')).join(', ') + } + + const getCssShadowFilter = (input) => { + if (input.length === 0) { + return 'none' + } + + return input + // drop-shadow doesn't support inset or spread + .filter((shad) => !shad.inset && Number(shad.spread) === 0) + .map((shad) => [ + shad.x, + shad.y, + // drop-shadow's blur is twice as strong compared to box-shadow + shad.blur / 2 + ].map(_ => _ + 'px').concat([ + cssColorString(findColor(shad.color), shad.alpha) + ]).join(' ')) + .map(_ => `drop-shadow(${_})`) + .join(' ') + } + const processInnerComponent = (component, parent) => { const { validInnerComponents = [], @@ -210,6 +327,7 @@ export const init = (extraRuleset, palette) => { name } = component + // Normalizing states and variants to always include "normal" const states = { normal: '', ...originalStates } const variants = { normal: '', ...originalVariants } const innerComponents = validInnerComponents.map(name => components[name]) @@ -219,13 +337,24 @@ export const init = (extraRuleset, palette) => { return stateCombinations.map(state => ({ variant, state })) }).reduce((acc, x) => [...acc, ...x], []) - const VIRTUAL_COMPONENTS = new Set(['Text', 'Link', 'Icon']) - stateVariantCombination.forEach(combination => { - let needRuleAdd = false + const soloSelector = ruleToSelector({ component: component.name, ...combination }, true) + const selector = ruleToSelector({ component: component.name, ...combination, parent }, true) - if (VIRTUAL_COMPONENTS.has(component.name)) { - const selector = component.name + ruleToSelector({ component: component.name, ...combination }) + // Inheriting all of the applicable rules + const existingRules = ruleset.filter(findRules({ component: component.name, ...combination, parent })) + const { directives: computedDirectives } = existingRules.reduce((acc, rule) => merge(acc, rule), {}) + const computedRule = { + component: component.name, + ...combination, + parent, + directives: computedDirectives + } + + computed[selector] = computed[selector] || {} + computed[selector].computedRule = computedRule + + if (virtualComponents.has(component.name)) { const virtualName = [ '--', component.name.toLowerCase(), @@ -235,52 +364,46 @@ export const init = (extraRuleset, palette) => { ...combination.state.filter(x => x !== 'normal').toSorted().map(state => state[0].toUpperCase() + state.slice(1).toLowerCase()) ].join('') - const lowerLevel = findLowerLevelRule(parent, (r) => { - if (!r) return false - if (components[r.component].validInnerComponents.indexOf(component.name) < 0) return false - if (r.cache.background === undefined) return false - if (r.cache.textDefined) { - return !r.cache.textDefined[selector] + let inheritedTextColor = computedDirectives.textColor + let inheritedTextOpacity = computedDirectives.textOpacity + let inheritedTextOpacityMode = computedDirectives.textOpacityMode + const lowerLevelTextSelector = [...selector.split(/ /g).slice(0, -1), soloSelector].join(' ') + const lowerLevelTextRule = computed[lowerLevelTextSelector] + + if (inheritedTextColor == null || inheritedTextOpacity == null || inheritedTextOpacityMode == null) { + inheritedTextColor = computedDirectives.textColor ?? lowerLevelTextRule.textColor + inheritedTextOpacity = computedDirectives.textOpacity ?? lowerLevelTextRule.textOpacity + inheritedTextOpacityMode = computedDirectives.textOpacityMode ?? lowerLevelTextRule.textOpacityMode + } + + const newTextRule = { + ...computedRule, + directives: { + ...computedRule.directives, + textColor: inheritedTextColor, + textOpacity: inheritedTextOpacity, + textOpacityMode: inheritedTextOpacityMode } - return true - }) - - if (!lowerLevel) return - - let inheritedTextColorRule - const inheritedTextColorRules = findLowerLevelRule(parent, (r) => { - return r.cache?.textDefined?.[selector] - }) - - if (!inheritedTextColorRule) { - const generalTextColorRules = ruleset.filter(findRules({ component: component.name, ...combination }, null, true)) - inheritedTextColorRule = generalTextColorRules.reduce((acc, rule) => merge(acc, rule), {}) - } else { - inheritedTextColorRule = inheritedTextColorRules.reduce((acc, rule) => merge(acc, rule), {}) } - let inheritedTextColor - let inheritedTextOpacity = {} - if (inheritedTextColorRule) { - inheritedTextColor = findColor(inheritedTextColorRule.directives.textColor, convert(lowerLevel.cache.background).rgb) - // also inherit opacity settings - const { textOpacity, textOpacityMode } = inheritedTextColorRule.directives - inheritedTextOpacity = { textOpacity, textOpacityMode } - } else { - // Emergency fallback - inheritedTextColor = '#000000' - } + const lowerLevelSelector = selector.split(/ /g).slice(0, -1).join(' ') + const lowerLevelBackground = cache[lowerLevelSelector].background const textColor = getTextColor( - convert(lowerLevel.cache.background).rgb, - convert(inheritedTextColor).rgb, - component.name === 'Link' // make it configurable? + convert(lowerLevelBackground).rgb, + // TODO properly provide "parent" text color? + convert(findColor(inheritedTextColor, null, lowerLevelBackground)).rgb, + true // component.name === 'Link' || combination.variant === 'greentext' // make it configurable? ) - lowerLevel.cache.textDefined = lowerLevel.cache.textDefined || {} - lowerLevel.cache.textDefined[selector] = textColor - lowerLevel.virtualDirectives = lowerLevel.virtualDirectives || {} - lowerLevel.virtualDirectives[virtualName] = getTextColorAlpha(inheritedTextColorRule, lowerLevel, textColor) + // Storing color data in lower layer to use as custom css properties + cache[lowerLevelSelector].textDefined = cache[lowerLevelSelector].textDefined || {} + cache[lowerLevelSelector].textDefined[selector] = textColor + + const virtualDirectives = {} + virtualDirectives[virtualName] = getTextColorAlpha(newTextRule, lowerLevelBackground, textColor) + + // lastRule.computed = lastRule.computed || {} const directives = { textColor, @@ -288,45 +411,54 @@ export const init = (extraRuleset, palette) => { } // Debug: lets you see what it think background color should be - directives.background = convert(lowerLevel.cache.background).hex + // directives.background = convert(cache[lowerLevelSelector].background).hex addRule({ parent, virtual: true, component: component.name, ...combination, - cache: { background: lowerLevel.cache.background }, - directives + directives, + virtualDirectives }) } else { - const existingGlobalRules = ruleset.filter(findRules({ component: component.name, ...combination }, null)) - const existingRules = ruleset.filter(findRules({ component: component.name, ...combination }, parent)) + cache[selector] = cache[selector] || {} + computed[selector] = computed[selector] || {} - // Global (general) rules - if (existingGlobalRules.length !== 0) { - const totalRule = existingGlobalRules.reduce((acc, rule) => merge(acc, rule), {}) - const { directives } = totalRule - - // last rule is used as a cache - const lastRule = existingGlobalRules[existingGlobalRules.length - 1] - lastRule.cache = lastRule.cache || {} - - if (directives.background) { - const rgb = convert(findColor(directives.background)).rgb - - // TODO: DEFAULT TEXT COLOR - const bg = findLowerLevelRule(parent)?.cache.background || convert('#FFFFFF').rgb - - if (!lastRule.cache.background) { - const blend = directives.opacity < 1 ? alphaBlend(rgb, directives.opacity, bg) : rgb - lastRule.cache.background = blend - - needRuleAdd = true - } + if (computedDirectives.background) { + let inheritRule = null + const variantRules = ruleset.filter(findRules({ component: component.name, variant: combination.variant, parent })) + const lastVariantRule = variantRules[variantRules.length - 1] + if (lastVariantRule) { + inheritRule = lastVariantRule + } else { + const normalRules = ruleset.filter(findRules({ component: component.name, parent })) + const lastNormalRule = normalRules[normalRules.length - 1] + inheritRule = lastNormalRule } - if (needRuleAdd) { - addRule(lastRule) + const inheritSelector = ruleToSelector({ ...inheritRule, parent }, true) + const inheritedBackground = cache[inheritSelector].background + const lowerLevelSelector = selector.split(/ /g).slice(0, -1).join(' ') + + // TODO: DEFAULT TEXT COLOR + const bg = cache[lowerLevelSelector]?.background || convert('#FFFFFF').rgb + + console.log('SELECTOR', lowerLevelSelector) + + const rgb = convert(findColor(computedDirectives.background, inheritedBackground, cache[lowerLevelSelector].background)).rgb + + if (!cache[selector].background) { + const blend = computedDirectives.opacity < 1 ? alphaBlend(rgb, computedDirectives.opacity, bg) : rgb + cache[selector].background = blend + computed[selector].background = rgb + + addRule({ + component: component.name, + ...combination, + parent, + directives: computedDirectives + }) } } @@ -338,12 +470,12 @@ export const init = (extraRuleset, palette) => { }) } - processInnerComponent(components[rootName]) + processInnerComponent(components.Root, { component: 'Root' }) return { raw: rules, css: rules.map(rule => { - if (rule.virtual) return '' + // if (rule.virtual) return '' let selector = ruleToSelector(rule).replace(/\/\*.*\*\//g, '') if (!selector) { @@ -356,22 +488,38 @@ export const init = (extraRuleset, palette) => { return ' ' + k + ': ' + v }).join(';\n') - const directives = Object.entries(rule.directives).map(([k, v]) => { - switch (k) { - case 'background': { - return 'background-color: ' + rgba2css({ ...convert(findColor(v)).rgb, a: rule.directives.opacity ?? 1 }) + let directives + if (rule.component !== 'Root') { + directives = Object.entries(rule.directives).map(([k, v]) => { + switch (k) { + case 'shadow': { + return ' ' + [ + '--shadow: ' + getCssShadow(v), + '--shadowFilter: ' + getCssShadowFilter(v), + '--shadowInset: ' + getCssShadow(v, true) + ].join(';\n ') + } + case 'background': { + const color = cssColorString(computed[ruleToSelector(rule, true)].background, rule.directives.opacity) + return [ + 'background-color: ' + color, + ' --background: ' + color + ].join(';\n') + } + case 'textColor': { + return 'color: ' + v + } + default: return '' } - case 'textColor': { - return 'color: ' + v - } - default: return '' - } - }).filter(x => x).map(x => ' ' + x).join(';\n') + }).filter(x => x).map(x => ' ' + x).join(';\n') + } else { + directives = {} + } return [ header, directives + ';', - ' color: var(--text);', + !rule.virtual ? ' color: var(--text);' : '', '', virtualDirectives, footer diff --git a/top_bar.style.js b/top_bar.style.js new file mode 100644 index 000000000..91b2e72b6 --- /dev/null +++ b/top_bar.style.js @@ -0,0 +1,9 @@ +export default { + name: 'TopBar', + selector: 'nav', + validInnerComponents: [ + 'Link', + 'Text', + 'Icon' + ] +} From d2a74ea1a2965eb38609bb8029a41b45beab89ce Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 7 Feb 2024 15:53:49 +0200 Subject: [PATCH 021/678] add .input class to all inputs --- .../announcement_editor.vue | 4 ++- src/components/autosuggest/autosuggest.vue | 2 +- src/components/chat_new/chat_new.vue | 1 + src/components/checkbox/checkbox.vue | 25 ++++++++-------- src/components/desktop_nav/desktop_nav.scss | 30 +++---------------- src/components/emoji_input/emoji_input.vue | 9 ++++-- src/components/emoji_picker/emoji_picker.vue | 2 +- src/components/font_control/font_control.vue | 4 +-- .../image_cropper/image_cropper.vue | 2 +- src/components/importer/importer.vue | 1 + src/components/lists_edit/lists_edit.vue | 1 + .../lists_user_search/lists_user_search.vue | 1 + src/components/login_form/login_form.vue | 4 +-- src/components/mention_link/mention_link.vue | 2 +- src/components/mfa_form/recovery_form.vue | 2 +- src/components/mfa_form/totp_form.vue | 2 +- src/components/mobile_nav/mobile_nav.vue | 2 +- .../opacity_input/opacity_input.vue | 2 +- .../password_reset/password_reset.vue | 2 +- src/components/poll/poll.vue | 4 ++- src/components/poll/poll_form.vue | 4 +-- .../post_status_form/post_status_form.vue | 8 +++-- src/components/range_input/range_input.vue | 6 ++-- src/components/registration/registration.vue | 20 ++++++------- src/components/report/report.vue | 2 +- src/components/search/search.vue | 2 +- src/components/search_bar/search_bar.vue | 2 +- .../helpers/attachment_setting.vue | 2 +- .../settings_modal/helpers/number_setting.vue | 2 +- .../settings_modal/helpers/size_setting.vue | 2 +- .../settings_modal/helpers/string_setting.vue | 2 +- .../settings_modal/tabs/filtering_tab.vue | 2 +- .../settings_modal/tabs/profile_tab.vue | 10 +++++-- .../settings_modal/tabs/security_tab/mfa.vue | 2 ++ .../tabs/security_tab/mfa_totp.vue | 1 + .../tabs/security_tab/security_tab.vue | 9 ++++++ .../settings_modal/tabs/theme_tab/preview.vue | 2 ++ .../shadow_control/shadow_control.vue | 18 +++++------ src/components/user_card/user_card.vue | 4 +-- src/components/user_note/user_note.vue | 2 +- .../user_reporting_modal.vue | 2 +- 41 files changed, 108 insertions(+), 98 deletions(-) diff --git a/src/components/announcement_editor/announcement_editor.vue b/src/components/announcement_editor/announcement_editor.vue index 0f29f9f70..017b6f5f9 100644 --- a/src/components/announcement_editor/announcement_editor.vue +++ b/src/components/announcement_editor/announcement_editor.vue @@ -3,7 +3,7 @@ + +
    diff --git a/src/i18n/en.json b/src/i18n/en.json index 64d6201df..df0f8d6db 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -966,6 +966,9 @@ "blur": "Blur", "spread": "Spread", "inset": "Inset", + "raw": "Plain shadow", + "expression": "Expression (advanced)", + "empty_expression": "Empty expression", "hintV3": "For shadows you can also use the {0} notation to use other color slot.", "filter_hint": { "always_drop_shadow": "Warning, this shadow always uses {0} when browser supports it.", diff --git a/src/services/theme_data/theme_data_3.service.js b/src/services/theme_data/theme_data_3.service.js index 4765a7733..e45d2cefd 100644 --- a/src/services/theme_data/theme_data_3.service.js +++ b/src/services/theme_data/theme_data_3.service.js @@ -44,8 +44,8 @@ const findShadow = (shadows, { dynamicVars, staticVars }) => { if (shadow.startsWith('$')) { targetShadow = process(shadow, shadowFunctions, { findColor, findShadow }, { dynamicVars, staticVars }) } else if (shadow.startsWith('--')) { - const [variable] = shadow.split(/,/g).map(str => str.trim()) // discarding modifier since it's not supported - const variableSlot = variable.substring(2) + // modifiers are completely unsupported here + const variableSlot = shadow.substring(2) return findShadow(staticVars[variableSlot], { dynamicVars, staticVars }) } else { targetShadow = parseShadow(shadow) @@ -66,6 +66,7 @@ const findColor = (color, { dynamicVars, staticVars }) => { if (typeof color !== 'string' || (!color.startsWith('--') && !color.startsWith('$'))) return color let targetColor = null if (color.startsWith('--')) { + // Modifier support is pretty much for v2 themes only const [variable, modifier] = color.split(/,/g).map(str => str.trim()) const variableSlot = variable.substring(2) if (variableSlot === 'stack') { @@ -421,7 +422,7 @@ export const init = ({ break } case 'shadow': { - const shadow = value.split(/,/g).map(s => s.trim()) + const shadow = value.split(/,/g).map(s => s.trim()).filter(x => x) dynamicVars[k] = shadow if (combination.component === rootComponentName) { staticVars[k.substring(2)] = shadow From 3d77860e57892569cfe98bccfdc65d3a9c95dbf6 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Fri, 4 Oct 2024 02:49:20 +0300 Subject: [PATCH 419/678] moved the select motion stuff into its own component --- src/components/select/select_motion.vue | 115 +++++ .../tabs/style_tab/style_tab.vue | 432 +++++++++--------- .../shadow_control/shadow_control.js | 45 +- .../shadow_control/shadow_control.vue | 55 +-- 4 files changed, 356 insertions(+), 291 deletions(-) create mode 100644 src/components/select/select_motion.vue diff --git a/src/components/select/select_motion.vue b/src/components/select/select_motion.vue new file mode 100644 index 000000000..1b9f4041a --- /dev/null +++ b/src/components/select/select_motion.vue @@ -0,0 +1,115 @@ + + + + + diff --git a/src/components/settings_modal/tabs/style_tab/style_tab.vue b/src/components/settings_modal/tabs/style_tab/style_tab.vue index 590624ffd..7026f7afb 100644 --- a/src/components/settings_modal/tabs/style_tab/style_tab.vue +++ b/src/components/settings_modal/tabs/style_tab/style_tab.vue @@ -51,231 +51,241 @@
-
-
- - -
+
- - - {{ fallbackI18n($t(getVariantPath(selectedComponentName, variant)), variant) }} - - -
-
- -
    + {{ fallbackI18n($t(getFriendlyNamePath(componentsMap.get(key).name)), componentsMap.get(key).name) }} + + +
+
-
  • + {{ $t('settings.style.themes3.editor.variant_selector') }} + + +
  • +
    + +
      +
    • + + {{ fallbackI18n($t(getStatePath(selectedComponentName, state)), state) }} + +
    • +
    +
    +
    + + + + +
    + +
    + + + + + + + + + + + + +
    + + +
    + + + +
    + +
    +
    + +
    + + + + + + + + +
    +
    - {{ fallbackI18n($t(getStatePath(selectedComponentName, state)), state) }} - - - + {{ $t('settings.style.themes3.editor.include_in_rule') }} + + +
    +
    -
    - - - - -
    - -
    - - - - - - - - - - + + + - - - -
    - - - -
    - -
    -
    - -
    - - - - - - - - + {{ $t('settings.style.themes3.palette.light') }} + + -
    - - {{ $t('settings.style.themes3.editor.include_in_rule') }} - - -
    -
    - -
    -
    - - +
    - -
    + diff --git a/src/components/shadow_control/shadow_control.js b/src/components/shadow_control/shadow_control.js index 2befb8ba2..fc227b5be 100644 --- a/src/components/shadow_control/shadow_control.js +++ b/src/components/shadow_control/shadow_control.js @@ -1,6 +1,7 @@ import ColorInput from 'src/components/color_input/color_input.vue' import OpacityInput from 'src/components/opacity_input/opacity_input.vue' import Select from 'src/components/select/select.vue' +import SelectMotion from 'src/components/select/select_motion.vue' import Checkbox from 'src/components/checkbox/checkbox.vue' import Popover from 'src/components/popover/popover.vue' import ComponentPreview from 'src/components/component_preview/component_preview.vue' @@ -54,13 +55,11 @@ export default { ColorInput, OpacityInput, Select, + SelectMotion, Checkbox, Popover, ComponentPreview }, - beforeUpdate () { - this.cValue = (this.modelValue ?? this.fallback ?? []).map(toModel) - }, computed: { selectedType: { get () { @@ -73,7 +72,6 @@ export default { selected: { get () { const selected = this.cValue[this.selectedId] - console.log('SELECTED', selected) if (selected && typeof selected === 'object') { return { ...selected } } else if (typeof selected === 'string') { @@ -95,12 +93,6 @@ export default { currentFallback () { return this.fallback?.[this.selectedId] }, - moveUpValid () { - return this.selectedId > 0 - }, - moveDnValid () { - return this.selectedId < this.cValue.length - 1 - }, usingFallback () { return this.modelValue == null }, @@ -123,11 +115,20 @@ export default { } }, watch: { + modelValue (value) { + if (!value) this.cValue = (this.modelValue ?? this.fallback ?? []).map(toModel) + }, selected (value) { this.$emit('subShadowSelected', this.selectedId) } }, methods: { + getNewSubshadow () { + return toModel(this.selected) + }, + onSelectChange (id) { + this.selectedId = id + }, getSubshadowLabel (shadow, index) { if (typeof shadow === 'object') { return shadow?.name ?? this.$t('settings.style.shadows.shadow_id', { value: index }) @@ -141,28 +142,6 @@ export default { this.cValue[this.selectedId].spread = 0 } this.$emit('update:modelValue', this.cValue) - }, 100), - add () { - this.cValue.push(toModel(this.selected)) - this.selectedId = Math.max(this.cValue.length - 1, 0) - this.$emit('update:modelValue', this.cValue) - }, - del () { - this.cValue.splice(this.selectedId, 1) - this.selectedId = this.cValue.length === 0 ? undefined : Math.max(this.selectedId - 1, 0) - this.$emit('update:modelValue', this.cValue) - }, - moveUp () { - const movable = this.cValue.splice(this.selectedId, 1)[0] - this.cValue.splice(this.selectedId - 1, 0, movable) - this.selectedId -= 1 - this.$emit('update:modelValue', this.cValue) - }, - moveDn () { - const movable = this.cValue.splice(this.selectedId, 1)[0] - this.cValue.splice(this.selectedId + 1, 0, movable) - this.selectedId += 1 - this.$emit('update:modelValue', this.cValue) - } + }, 100) } } diff --git a/src/components/shadow_control/shadow_control.vue b/src/components/shadow_control/shadow_control.vue index 669de36e1..6a5cd1861 100644 --- a/src/components/shadow_control/shadow_control.vue +++ b/src/components/shadow_control/shadow_control.vue @@ -28,53 +28,14 @@ {{ getSubshadowLabel(shadow, index) }} -
    - - - - -
    +
    - + /> diff --git a/src/services/export_import/export_import.js b/src/services/export_import/export_import.js index e1ac6a425..b8f84351b 100644 --- a/src/services/export_import/export_import.js +++ b/src/services/export_import/export_import.js @@ -2,15 +2,22 @@ import utf8 from 'utf8' export const newExporter = ({ filename = 'data', + mime = 'application/json', + extension = '.json', getExportedObject }) => ({ exportData () { - const stringified = utf8.encode(JSON.stringify(getExportedObject(), null, 2)) // Pretty-print and indent with 2 spaces + let stringified + if (mime === 'application/json') { + stringified = utf8.encode(JSON.stringify(getExportedObject(), null, 2)) // Pretty-print and indent with 2 spaces + } else { + stringified = utf8.encode(getExportedObject()) // Pretty-print and indent with 2 spaces + } // Create an invisible link with a data url and simulate a click const e = document.createElement('a') - e.setAttribute('download', `${filename}.json`) - e.setAttribute('href', 'data:application/json;base64,' + window.btoa(stringified)) + e.setAttribute('download', `${filename}.${extension}`) + e.setAttribute('href', `data:${mime};base64, ${window.btoa(stringified)}`) e.style.display = 'none' document.body.appendChild(e) From 756ea63b6709e3daf35d738b0a1f3a5e9439c4d9 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Sun, 6 Oct 2024 20:21:13 +0300 Subject: [PATCH 435/678] variables stuff seem to be at least somewhat working --- .../palette_editor/palette_editor.vue | 2 +- .../tabs/style_tab/style_tab.js | 36 +++++++++++++++---- .../tabs/style_tab/style_tab.scss | 8 ++--- .../tabs/style_tab/style_tab.vue | 9 +++-- .../shadow_control/shadow_control.js | 4 +++ src/i18n/en.json | 6 +++- 6 files changed, 51 insertions(+), 14 deletions(-) diff --git a/src/components/palette_editor/palette_editor.vue b/src/components/palette_editor/palette_editor.vue index cebe73ee1..16148262c 100644 --- a/src/components/palette_editor/palette_editor.vue +++ b/src/components/palette_editor/palette_editor.vue @@ -107,7 +107,7 @@ const updatePalette = (paletteKey, value) => { grid-template-columns: repeat(4, 1fr); grid-template-rows: repeat(3, 1fr) auto; grid-gap: 0.5em; - align-items: space-between; + align-items: baseline; .palette-import-button { grid-column: 1 / span 2; diff --git a/src/components/settings_modal/tabs/style_tab/style_tab.js b/src/components/settings_modal/tabs/style_tab/style_tab.js index 4e9a39060..c75e6b6b4 100644 --- a/src/components/settings_modal/tabs/style_tab/style_tab.js +++ b/src/components/settings_modal/tabs/style_tab/style_tab.js @@ -17,7 +17,7 @@ import ContrastRatio from 'src/components/contrast_ratio/contrast_ratio.vue' import { init } from 'src/services/theme_data/theme_data_3.service.js' import { getCssRules } from 'src/services/theme_data/css_utils.js' import { serialize } from 'src/services/theme_data/iss_serializer.js' -// import { deserialize } from 'src/services/theme_data/iss_deserializer.js' +import { parseShadow /* , deserialize */ } from 'src/services/theme_data/iss_deserializer.js' import { // rgb2hex, hex2rgb, @@ -259,7 +259,7 @@ export default { } }) - const getEditedElement = (component, directive) => computed({ + const getEditedElement = (component, directive, postProcess = x => x) => computed({ get () { let usedRule const fallback = editorFriendlyFallbackStructure.value @@ -271,7 +271,11 @@ export default { usedRule = get(fallback, path) } - return usedRule + if (directive === 'shadow') { + console.log('EDITED', usedRule) + console.log('PP', postProcess(usedRule)) + } + return postProcess(usedRule) }, set (value) { set(allEditedRules, getPath(component, directive), value) @@ -316,9 +320,22 @@ export default { } } + const normalizeShadows = (shadows) => { + console.log('NORMALIZE') + return shadows?.map(shadow => { + if (typeof shadow === 'object') { + return shadow + } + if (typeof shadow === 'string') { + return parseShadow(shadow) + } + return null + }) + } + // Shadow is partially edited outside the ShadowControl // for better space utilization - const editedShadow = getEditedElement(null, 'shadow') + const editedShadow = getEditedElement(null, 'shadow', normalizeShadows) const editedSubShadowId = ref(null) const editedSubShadow = computed(() => { if (editedShadow.value == null || editedSubShadowId.value == null) return null @@ -511,8 +528,15 @@ export default { const selectedVirtualDirectiveParsed = computed({ get () { switch (selectedVirtualDirective.value.valType) { - case 'shadow': - return {} + case 'shadow': { + const directiveValue = selectedVirtualDirective.value.value + if (Array.isArray(directiveValue)) { + return normalizeShadows(directiveValue) + } else { + const splitShadow = directiveValue.split(/,/g).map(x => x.trim()) + return normalizeShadows(splitShadow) + } + } default: return null } diff --git a/src/components/settings_modal/tabs/style_tab/style_tab.scss b/src/components/settings_modal/tabs/style_tab/style_tab.scss index 0f8540f9a..0384f7a48 100644 --- a/src/components/settings_modal/tabs/style_tab/style_tab.scss +++ b/src/components/settings_modal/tabs/style_tab/style_tab.scss @@ -93,7 +93,7 @@ "label editor" "selector editor" "movement editor"; - grid-template-columns: auto 1fr; + grid-template-columns: 10em 1fr; grid-template-rows: auto 1fr auto; grid-gap: 0.5em; @@ -124,9 +124,9 @@ grid-template-rows: auto auto 1fr; grid-gap: 0.5em; grid-template-areas: - "component component variant" - "state state state" - "preview settings settings"; + "component component variant" + "state state state" + "preview settings settings"; .component-selector { grid-area: component; diff --git a/src/components/settings_modal/tabs/style_tab/style_tab.vue b/src/components/settings_modal/tabs/style_tab/style_tab.vue index 8aaa67130..430b7c614 100644 --- a/src/components/settings_modal/tabs/style_tab/style_tab.vue +++ b/src/components/settings_modal/tabs/style_tab/style_tab.vue @@ -245,6 +245,7 @@ > {{ $t('settings.style.themes3.editor.include_in_rule') }} + {{ editedShadow }}
    + - {{ selectedVirtualDirective.valType }}
    diff --git a/src/components/shadow_control/shadow_control.js b/src/components/shadow_control/shadow_control.js index fc227b5be..3fe1aa299 100644 --- a/src/components/shadow_control/shadow_control.js +++ b/src/components/shadow_control/shadow_control.js @@ -45,6 +45,7 @@ export default { ], emits: ['update:modelValue', 'subShadowSelected'], data () { + console.log('MODEL VALUE', this.modelValue, this.fallback) return { selectedId: 0, // TODO there are some bugs regarding display of array (it's not getting updated when deleting for some reason) @@ -60,6 +61,9 @@ export default { Popover, ComponentPreview }, + beforeUpdate () { + this.cValue = (this.modelValue ?? this.fallback ?? []).map(toModel) + }, computed: { selectedType: { get () { diff --git a/src/i18n/en.json b/src/i18n/en.json index 3694a3908..bad4ddb03 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -774,6 +774,9 @@ "extra3": "Extra 3", "v2_unsupported": "Older v2 themes don't support palettes. Switch to v3 theme to make use of palettes" }, + "variables": { + "label": "Variables" + }, "editor": { "title": "Style", "new_style": "New", @@ -800,7 +803,8 @@ "no-auto": "Disabled" }, "component_tab": "Components style", - "palette_tab": "Color presets" + "palette_tab": "Color presets", + "variables_tab": "Variables (Advanced)" }, "hacks": { "underlay_overrides": "Change underlay", From cfe52185f74684ebb599754ebb5b888302498231 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 7 Oct 2024 00:57:54 +0300 Subject: [PATCH 436/678] neat-looking variables tab (sans shadow editor) --- .../tabs/style_tab/style_tab.scss | 16 ++++ .../tabs/style_tab/style_tab.vue | 90 ++++++++++++------- src/i18n/en.json | 4 +- 3 files changed, 75 insertions(+), 35 deletions(-) diff --git a/src/components/settings_modal/tabs/style_tab/style_tab.scss b/src/components/settings_modal/tabs/style_tab/style_tab.scss index 0384f7a48..69349f785 100644 --- a/src/components/settings_modal/tabs/style_tab/style_tab.scss +++ b/src/components/settings_modal/tabs/style_tab/style_tab.scss @@ -109,6 +109,7 @@ font-weight: bold; grid-area: label; margin: 0; + align-self: baseline; } &-movement { @@ -118,6 +119,21 @@ } } + .variables-editor { + .variable-selector { + display: grid; + grid-template-columns: auto 1fr auto 10em; + grid-template-rows: subgrid; + align-items: baseline; + grid-gap: 0 0.5em; + } + + .list-edit-area { + display: grid; + grid-template-rows: subgrid; + } + } + .component-editor { display: grid; grid-template-columns: 6fr 3fr 4fr; diff --git a/src/components/settings_modal/tabs/style_tab/style_tab.vue b/src/components/settings_modal/tabs/style_tab/style_tab.vue index 430b7c614..9b5a4f9e6 100644 --- a/src/components/settings_modal/tabs/style_tab/style_tab.vue +++ b/src/components/settings_modal/tabs/style_tab/style_tab.vue @@ -259,7 +259,7 @@
    diff --git a/src/components/shadow_control/shadow_control.js b/src/components/shadow_control/shadow_control.js index 3fe1aa299..8eab5c917 100644 --- a/src/components/shadow_control/shadow_control.js +++ b/src/components/shadow_control/shadow_control.js @@ -41,7 +41,12 @@ const toModel = (input) => { export default { props: [ - 'modelValue', 'fallback', 'separateInset', 'noPreview', 'disabled' + 'modelValue', + 'fallback', + 'separateInset', + 'noPreview', + 'disabled', + 'compact' ], emits: ['update:modelValue', 'subShadowSelected'], data () { diff --git a/src/components/shadow_control/shadow_control.scss b/src/components/shadow_control/shadow_control.scss index de4159c11..b0cf70148 100644 --- a/src/components/shadow_control/shadow_control.scss +++ b/src/components/shadow_control/shadow_control.scss @@ -1,12 +1,32 @@ -.settings-modal .settings-modal-panel .shadow-control { - display: flex; - flex-wrap: wrap; +.ShadowControl { + display: grid; + grid-template-columns: 10em 1fr 1fr; + grid-template-rows: 1fr; + grid-template-areas: "selector preview tweak"; + grid-gap: 0.5em; justify-content: stretch; - grid-gap: 0.25em; margin-bottom: 1em; width: 100%; + &.-compact { + grid-template-columns: 1fr; + grid-template-rows: 10em auto auto; + grid-template-areas: + "selector" + "preview" + "tweak"; + + &.-no-preview { + grid-template-columns: 1fr; + grid-template-rows: 10em 1fr; + grid-template-areas: + "selector" + "tweak"; + } + } + .shadow-switcher { + grid-area: selector; order: 1; flex: 1 0 6em; min-width: 6em; @@ -20,6 +40,7 @@ } .shadow-tweak { + grid-area: tweak; order: 3; flex: 2 0 10em; min-width: 10em; @@ -65,6 +86,10 @@ } &.-no-preview { + grid-template-columns: 10em 1fr; + grid-template-rows: 1fr; + grid-template-areas: "selector tweak"; + .shadow-tweak { order: 0; flex: 2 0 8em; @@ -87,8 +112,7 @@ } .shadow-preview { - order: 2; - flex: 3 3 15em; + grid-area: preview; min-width: 10em; margin-left: 0.125em; align-self: start; diff --git a/src/components/shadow_control/shadow_control.vue b/src/components/shadow_control/shadow_control.vue index 29adfff4a..4f0906c70 100644 --- a/src/components/shadow_control/shadow_control.vue +++ b/src/components/shadow_control/shadow_control.vue @@ -1,7 +1,7 @@