From 24b66777eaea442a5e323ca4c57b4e9decf6a345 Mon Sep 17 00:00:00 2001 From: Victor Ilyushchenko Date: Fri, 10 Jan 2025 12:32:30 +0300 Subject: [PATCH] Text & Table cell color styling features (#7624) Signed-off-by: Victor Ilyushchenko --- common/config/rush/pnpm-lock.yaml | 12 +- models/text-editor/src/index.ts | 29 +++- models/text-editor/src/plugin.ts | 6 +- packages/text/package.json | 3 +- packages/text/src/index.ts | 3 + packages/text/src/kits/server-kit.ts | 7 +- packages/text/src/marks/colors.ts | 142 ++++++++++++++++++ packages/theme/styles/_colors.scss | 44 ++++++ packages/theme/styles/prose.scss | 2 +- plugins/text-editor-assets/assets/icons.svg | 7 + plugins/text-editor-assets/lang/en.json | 4 +- plugins/text-editor-assets/lang/ru.json | 4 +- plugins/text-editor-assets/src/index.ts | 4 +- .../src/components/extension/colors.ts | 100 ++++++++++++ .../extension/popups/ColorPicker.svelte | 65 ++++++++ .../extension/table/TableNodeView.svelte | 2 +- .../extension/table/TableToolbar.svelte | 27 +++- plugins/text-editor-resources/src/index.ts | 7 +- .../src/kits/editor-kit.ts | 8 +- plugins/text-editor/src/plugin.ts | 13 +- plugins/text-editor/src/types.ts | 2 +- 21 files changed, 469 insertions(+), 22 deletions(-) create mode 100644 packages/text/src/marks/colors.ts create mode 100644 plugins/text-editor-resources/src/components/extension/colors.ts create mode 100644 plugins/text-editor-resources/src/components/extension/popups/ColorPicker.svelte diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index a5d4a5d887..cda5be48d4 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -6254,6 +6254,14 @@ packages: '@tiptap/core': 2.6.6(@tiptap/pm@2.6.6) dev: false + /@tiptap/extension-text-style@2.11.0(@tiptap/core@2.6.6): + resolution: {integrity: sha512-vuA16wMZ6J3fboL7FObwV2f5uN9Vg0WYmqU7971vxzJyaRj9VE1eeH8Kh5fq4RgwDzc13MZGvZZV4HcE1R8o8A==} + peerDependencies: + '@tiptap/core': ^2.7.0 + dependencies: + '@tiptap/core': 2.6.6(@tiptap/pm@2.6.6) + dev: false + /@tiptap/extension-text@2.6.6(@tiptap/core@2.6.6): resolution: {integrity: sha512-e84uILnRzNzcwK1DVQNpXVmBG1Cq3BJipTOIDl1LHifOok7MBjhI/X+/NR0bd3N2t6gmDTWi63+4GuJ5EeDmsg==} peerDependencies: @@ -32495,6 +32503,7 @@ packages: '@tiptap/extension-table-header': 2.6.6(@tiptap/core@2.6.6) '@tiptap/extension-table-row': 2.6.6(@tiptap/core@2.6.6) '@tiptap/extension-text-align': 2.11.0(@tiptap/core@2.6.6) + '@tiptap/extension-text-style': 2.11.0(@tiptap/core@2.6.6) '@tiptap/extension-typography': 2.6.6(@tiptap/core@2.6.6) '@tiptap/extension-underline': 2.6.6(@tiptap/core@2.6.6) '@tiptap/pm': 2.6.6 @@ -32639,7 +32648,7 @@ packages: dev: false file:projects/text.tgz(@types/node@20.11.19)(bufferutil@4.0.8)(esbuild@0.24.2)(ts-node@10.9.2)(utf-8-validate@6.0.4): - resolution: {integrity: sha512-zmM6GDzeNuMbMA7iWwhg8hBc43fylNW7cEmk0fSWw9+G9NqD2MLv2dVb5dnAcPAioj9WBdDakWcrLAAHUBJyMw==, tarball: file:projects/text.tgz} + resolution: {integrity: sha512-p2TRL6FgtqVBooDf+E5A4HB+45VS03IvNxudhC9gCotewS726ZLOxl2XdfeuYB2rHYbzdmmiG2zb8GbPwqTkOw==, tarball: file:projects/text.tgz} id: file:projects/text.tgz name: '@rush-temp/text' version: 0.0.0 @@ -32660,6 +32669,7 @@ packages: '@tiptap/extension-task-item': 2.6.6(@tiptap/core@2.6.6)(@tiptap/pm@2.6.6) '@tiptap/extension-task-list': 2.6.6(@tiptap/core@2.6.6) '@tiptap/extension-text-align': 2.11.0(@tiptap/core@2.6.6) + '@tiptap/extension-text-style': 2.11.0(@tiptap/core@2.6.6) '@tiptap/extension-typography': 2.6.6(@tiptap/core@2.6.6) '@tiptap/extension-underline': 2.6.6(@tiptap/core@2.6.6) '@tiptap/html': 2.6.6(@tiptap/core@2.6.6)(@tiptap/pm@2.6.6) diff --git a/models/text-editor/src/index.ts b/models/text-editor/src/index.ts index 028616d9c3..82557e4dfb 100644 --- a/models/text-editor/src/index.ts +++ b/models/text-editor/src/index.ts @@ -247,6 +247,18 @@ export function createModel (builder: Builder): void { index: 20 }) + builder.createDoc(textEditor.class.TextEditorAction, core.space.Model, { + action: textEditor.function.SetTextColor, + icon: textEditor.icon.TextStyle, + visibilityTester: textEditor.function.IsTextStylingEnabled, + isActive: { + name: 'textStyle' + }, + label: textEditor.string.SetTextColor, + category: 20, + index: 25 + }) + // Link category builder.createDoc(textEditor.class.TextEditorAction, core.space.Model, { action: textEditor.function.FormatLink, @@ -338,23 +350,36 @@ export function createModel (builder: Builder): void { index: 10 }) + // Table cell category + builder.createDoc(textEditor.class.TextEditorAction, core.space.Model, { + kind: 'table', + action: textEditor.function.SetBackgroundColor, + icon: textEditor.icon.Brush, + visibilityTester: textEditor.function.IsTableToolbarContext, + label: textEditor.string.SetCellHighlightColor, + category: 65, + index: 5 + }) + // Table category builder.createDoc(textEditor.class.TextEditorAction, core.space.Model, { + kind: 'table', action: textEditor.function.SelectTable, icon: textEditor.icon.SelectTable, visibilityTester: textEditor.function.IsTableToolbarContext, label: textEditor.string.SelectTable, category: 70, - index: 5 + index: 15 }) builder.createDoc(textEditor.class.TextEditorAction, core.space.Model, { + kind: 'table', action: textEditor.function.OpenTableOptions, icon: textEditor.icon.TableProps, visibilityTester: textEditor.function.IsTableToolbarContext, label: textEditor.string.TableOptions, category: 70, - index: 10 + index: 20 }) // Image align category diff --git a/models/text-editor/src/plugin.ts b/models/text-editor/src/plugin.ts index 5540a9240a..2039564663 100644 --- a/models/text-editor/src/plugin.ts +++ b/models/text-editor/src/plugin.ts @@ -36,9 +36,13 @@ export default mergeIds(textEditorId, textEditor, { IsTableToolbarContext: '' as Resource, IsEditableNote: '' as Resource, IsEditable: '' as Resource, + IsTextStylingEnabled: '' as Resource, IsHeadingVisible: '' as Resource, CreateInlineComment: '' as Resource, - ShouldShowCreateInlineCommentAction: '' as Resource + ShouldShowCreateInlineCommentAction: '' as Resource, + + SetBackgroundColor: '' as Resource, + SetTextColor: '' as Resource } }) diff --git a/packages/text/package.json b/packages/text/package.json index 0bf657add2..0147c859eb 100644 --- a/packages/text/package.json +++ b/packages/text/package.json @@ -64,7 +64,8 @@ "prosemirror-codemark": "^0.4.2", "markdown-it": "^14.0.0", "fast-equals": "^5.0.1", - "@tiptap/extension-text-align": "~2.11.0" + "@tiptap/extension-text-align": "~2.11.0", + "@tiptap/extension-text-style": "~2.11.0" }, "repository": "https://github.com/hcengineering/platform", "publishConfig": { diff --git a/packages/text/src/index.ts b/packages/text/src/index.ts index 87456d522c..d543c4f264 100644 --- a/packages/text/src/index.ts +++ b/packages/text/src/index.ts @@ -22,6 +22,7 @@ export * from './markup/utils' export * from './nodes' // export * from './ydoc' export * from './marks/code' +export * from './marks/colors' export * from './marks/noteBase' export * from './markdown' export * from './markdown/serializer' @@ -31,3 +32,5 @@ export * from './markdown/node' export * from './kits/default-kit' export * from './kits/server-kit' + +export { TextStyle, type TextStyleOptions } from '@tiptap/extension-text-style' diff --git a/packages/text/src/kits/server-kit.ts b/packages/text/src/kits/server-kit.ts index b6a7345bec..0226c673d5 100644 --- a/packages/text/src/kits/server-kit.ts +++ b/packages/text/src/kits/server-kit.ts @@ -34,6 +34,8 @@ import { CodeExtension, codeOptions } from '../marks/code' import { NoteBaseExtension } from '../marks/noteBase' import { CommentNode } from '../nodes/comment' import TextAlign from '@tiptap/extension-text-align' +import TextStyle from '@tiptap/extension-text-style' +import { BackgroundColor, TextColor } from '../marks/colors' const headingLevels: Level[] = [1, 2, 3, 4, 5, 6] @@ -97,7 +99,10 @@ export const ServerKit = Extension.create({ ReferenceNode, CommentNode, NodeUuid, - NoteBaseExtension + NoteBaseExtension, + TextStyle.configure({}), + TextColor.configure({}), + BackgroundColor.configure({ types: ['tableCell'] }) ] } }) diff --git a/packages/text/src/marks/colors.ts b/packages/text/src/marks/colors.ts new file mode 100644 index 0000000000..d228507a2d --- /dev/null +++ b/packages/text/src/marks/colors.ts @@ -0,0 +1,142 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { Extension } from '@tiptap/core' +import '@tiptap/extension-text-style' + +export interface BackgroundColorOptions { + types: string[] +} + +declare module '@tiptap/core' { + interface Commands { + colors: { + setTextColor: (color: string) => ReturnType + unsetTextColor: () => ReturnType + setBackgroundColor: (color: string) => ReturnType + unsetBackgroundColor: () => ReturnType + } + } +} + +export const BackgroundColor = Extension.create({ + name: 'backgroundColor', + + addOptions () { + return { + types: [] + } + }, + + addGlobalAttributes () { + return [ + { + types: this.options.types, + attributes: { + backgroundColor: { + parseHTML: (element) => { + return element.getAttribute('data-background-color') ?? undefined + }, + renderHTML: (attributes) => { + if (typeof attributes.backgroundColor !== 'string') { + return {} + } + + return { + 'data-background-color': attributes.backgroundColor, + style: `background-color: ${attributes.backgroundColor}` + } + } + } + } + } + ] + }, + + addCommands () { + return { + setBackgroundColor: + (backgroundColor: string) => + ({ commands }) => { + return this.options.types + .map((type) => commands.updateAttributes(type, { backgroundColor })) + .every((response) => response) + }, + + unsetBackgroundColor: + () => + ({ commands }) => { + return this.options.types + .map((type) => commands.resetAttributes(type, 'backgroundColor')) + .every((response) => response) + } + } + } +}) + +export interface TextColorOptions { + types: string[] +} + +export const TextColor = Extension.create({ + name: 'textColor', + + addOptions () { + return { + types: ['textStyle'] + } + }, + + addGlobalAttributes () { + return [ + { + types: this.options.types, + attributes: { + color: { + parseHTML: (element) => { + return element.getAttribute('data-color') ?? undefined + }, + renderHTML: (attributes) => { + if (typeof attributes.color !== 'string') { + return {} + } + + return { + 'data-color': attributes.color, + style: `color: ${attributes.color}` + } + } + } + } + } + ] + }, + + addCommands () { + return { + setTextColor: + (color: string) => + ({ chain }) => { + return chain().setMark('textStyle', { color }).run() + }, + + unsetTextColor: + () => + ({ chain }) => { + return chain().setMark('textStyle', { color: null }).removeEmptyTextStyle().run() + } + } + } +}) diff --git a/packages/theme/styles/_colors.scss b/packages/theme/styles/_colors.scss index bd095464bd..6e9132a7e8 100644 --- a/packages/theme/styles/_colors.scss +++ b/packages/theme/styles/_colors.scss @@ -248,6 +248,28 @@ --theme-text-editor-note-anchor-bg-primary: #688797; --theme-text-editor-note-anchor-bg-primary-light: #747C81; + --text-editor-table-border-color: hsl(220, 6%, 40%); + + --theme-text-editor-palette-text-gray: rgba(155, 155, 155, 1); + --theme-text-editor-palette-text-brown: rgba(186, 133, 111, 1); + --theme-text-editor-palette-text-orange: rgba(199, 125, 72, 1); + --theme-text-editor-palette-text-yellow: rgba(202, 152, 73, 1); + --theme-text-editor-palette-text-green: rgba(82, 158, 114, 1); + --theme-text-editor-palette-text-blue: rgba(94, 135, 201, 1); + --theme-text-editor-palette-text-purple: rgba(157, 104, 211, 1); + --theme-text-editor-palette-text-pink: rgba(209, 87, 150, 1); + --theme-text-editor-palette-text-red: rgba(223, 84, 82, 1); + + --theme-text-editor-palette-bg-gray: rgba(47, 47, 47, 1); + --theme-text-editor-palette-bg-brown: rgba(74, 50, 40, 1); + --theme-text-editor-palette-bg-orange: rgba(92, 59, 35, 1); + --theme-text-editor-palette-bg-yellow: rgba(86, 67, 40, 1); + --theme-text-editor-palette-bg-green: rgba(36, 61, 48, 1); + --theme-text-editor-palette-bg-blue: rgba(20, 58, 78, 1); + --theme-text-editor-palette-bg-purple: rgba(60, 45, 73, 1); + --theme-text-editor-palette-bg-pink: rgba(78, 44, 60, 1); + --theme-text-editor-palette-bg-red:rgba(82, 46, 42, 1); + --accent-bg-color: #27282b; --accent-shadow: rgb(0 0 0 / 10%) 0px 2px 4px; @@ -507,6 +529,28 @@ --theme-text-editor-note-anchor-bg-primary: #AAC5E9; --theme-text-editor-note-anchor-bg-primary-light: #D5E5F5; + --text-editor-table-border-color: #c9cbcd; + + --theme-text-editor-palette-text-gray: rgba(120, 119, 116, 1); + --theme-text-editor-palette-text-brown: rgba(159, 107, 83, 1); + --theme-text-editor-palette-text-orange: rgba(217, 115, 13, 1); + --theme-text-editor-palette-text-yellow: rgba(203, 145, 47, 1); + --theme-text-editor-palette-text-green: rgba(68, 131, 97, 1); + --theme-text-editor-palette-text-blue: rgba(51, 126, 169, 1); + --theme-text-editor-palette-text-purple: rgba(144, 101, 176, 1); + --theme-text-editor-palette-text-pink: rgba(193, 76, 138, 1); + --theme-text-editor-palette-text-red: rgba(212, 76, 71, 1); + + --theme-text-editor-palette-bg-gray: rgba(241, 241, 239, 1); + --theme-text-editor-palette-bg-brown: rgba(244, 238, 238, 1); + --theme-text-editor-palette-bg-orange: rgba(251, 236, 221, 1); + --theme-text-editor-palette-bg-yellow: rgba(251, 243, 219, 1); + --theme-text-editor-palette-bg-green: rgba(237, 243, 236, 1); + --theme-text-editor-palette-bg-blue: rgba(231, 243, 248, 1); + --theme-text-editor-palette-bg-purple: rgba(244, 240, 247, 0.8); + --theme-text-editor-palette-bg-pink: rgba(249, 238, 243, 0.8); + --theme-text-editor-palette-bg-red: rgba(253, 235, 236, 1); + --accent-bg-color: #eff0f2; // HZ --accent-shadow: rgb(0 0 0 / 10%) 0px 2px 4px; // Dark diff --git a/packages/theme/styles/prose.scss b/packages/theme/styles/prose.scss index 927528fa0e..9b2a92fa36 100644 --- a/packages/theme/styles/prose.scss +++ b/packages/theme/styles/prose.scss @@ -38,7 +38,7 @@ table.proseTable { th { min-width: 1rem; height: 2rem; - border: 1px solid var(--button-border-hover); + border: 1px solid var(--text-editor-table-border-color); padding: .25rem .5rem; vertical-align: top; box-sizing: border-box; diff --git a/plugins/text-editor-assets/assets/icons.svg b/plugins/text-editor-assets/assets/icons.svg index 56514789e4..4546048f49 100644 --- a/plugins/text-editor-assets/assets/icons.svg +++ b/plugins/text-editor-assets/assets/icons.svg @@ -214,4 +214,11 @@ + + + + + + + \ No newline at end of file diff --git a/plugins/text-editor-assets/lang/en.json b/plugins/text-editor-assets/lang/en.json index 8677c3e156..805f0b3eba 100644 --- a/plugins/text-editor-assets/lang/en.json +++ b/plugins/text-editor-assets/lang/en.json @@ -66,6 +66,8 @@ "MermaidDiargram": "Diagram", "Comment": "Comment", "AddComment": "Add a comment", - "AddCommentPlaceholder": "Add a comment..." + "AddCommentPlaceholder": "Add a comment...", + "SetCellHighlightColor": "Set cell color", + "SetTextColor": "Set text color" } } \ No newline at end of file diff --git a/plugins/text-editor-assets/lang/ru.json b/plugins/text-editor-assets/lang/ru.json index 704ab81366..a39503cef8 100644 --- a/plugins/text-editor-assets/lang/ru.json +++ b/plugins/text-editor-assets/lang/ru.json @@ -66,6 +66,8 @@ "MermaidDiargram": "Диаграмма", "Comment": "Комментарий", "AddComment": "Добавить комментарий", - "AddCommentPlaceholder": "Добавьте комментарий..." + "AddCommentPlaceholder": "Добавьте комментарий...", + "SetCellHighlightColor": "Изменить цвет ячеек", + "SetTextColor": "Изменить цвет текста" } } \ No newline at end of file diff --git a/plugins/text-editor-assets/src/index.ts b/plugins/text-editor-assets/src/index.ts index 0a4173dbd5..c1e7e3cb18 100644 --- a/plugins/text-editor-assets/src/index.ts +++ b/plugins/text-editor-assets/src/index.ts @@ -43,5 +43,7 @@ loadMetadata(textEditor.icon, { Comment: `${icons}#comment`, SelectTable: `${icons}#move`, MergeCells: `${icons}#union`, - SplitCells: `${icons}#divide` + SplitCells: `${icons}#divide`, + Brush: `${icons}#brush`, + TextStyle: `${icons}#textStyle` }) diff --git a/plugins/text-editor-resources/src/components/extension/colors.ts b/plugins/text-editor-resources/src/components/extension/colors.ts new file mode 100644 index 0000000000..aa162e140a --- /dev/null +++ b/plugins/text-editor-resources/src/components/extension/colors.ts @@ -0,0 +1,100 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { getEventPositionElement, showPopup } from '@hcengineering/ui' +import { type Editor } from '@tiptap/core' +import ColorPicker from './popups/ColorPicker.svelte' +import { type ActionContext } from '@hcengineering/text-editor' + +export interface BackgroundColorOptions { + types: string[] +} + +interface ColorSpec { + color: string + preview?: string +} + +function colorVar (tag: string, prefix = 'text'): string { + return `var(--theme-text-editor-palette-${prefix}-${tag})` +} + +function colorSpec (tag: string, prefix = 'text'): ColorSpec { + const color = colorVar(tag, prefix) + return { color, preview: colorVar(tag) } +} + +const palette = { + background: [ + { color: 'transparent' }, + colorSpec('gray', 'bg'), + colorSpec('brown', 'bg'), + colorSpec('orange', 'bg'), + colorSpec('yellow', 'bg'), + colorSpec('green', 'bg'), + colorSpec('blue', 'bg'), + colorSpec('purple', 'bg'), + colorSpec('pink', 'bg'), + colorSpec('red', 'bg') + ], + text: [ + { color: 'var(--theme-text-color-primary)' }, + colorSpec('gray'), + colorSpec('brown'), + colorSpec('orange'), + colorSpec('yellow'), + colorSpec('green'), + colorSpec('blue'), + colorSpec('purple'), + colorSpec('pink'), + colorSpec('red') + ] +} + +export async function openBackgroundColorOptions (editor: Editor, event: MouseEvent): Promise { + await new Promise((resolve) => { + showPopup(ColorPicker, { palette: palette.background }, getEventPositionElement(event), (val) => { + const color: string | undefined = val?.color + if (color === undefined) return + + if (color === 'transparent') { + editor.commands.unsetBackgroundColor() + } else { + editor.commands.setBackgroundColor(color) + } + resolve() + }) + }) +} + +export async function openTextColorOptions (editor: Editor, event: MouseEvent): Promise { + await new Promise((resolve) => { + showPopup(ColorPicker, { palette: palette.text }, getEventPositionElement(event), (val) => { + const color: string | undefined = val?.color + if (color === undefined) return + + if (color === 'var(--theme-text-color-primary)') { + editor.commands.unsetTextColor() + } else { + editor.commands.setTextColor(color) + } + resolve() + }) + }) +} + +export async function isTextStylingEnabled (editor: Editor, context: ActionContext): Promise { + return editor.isEditable && editor.commands.setTextColor !== undefined +} diff --git a/plugins/text-editor-resources/src/components/extension/popups/ColorPicker.svelte b/plugins/text-editor-resources/src/components/extension/popups/ColorPicker.svelte new file mode 100644 index 0000000000..83cf73cd92 --- /dev/null +++ b/plugins/text-editor-resources/src/components/extension/popups/ColorPicker.svelte @@ -0,0 +1,65 @@ + + + +
+
+ {#each palette as k} + + +
{ + handleSubmit(k) + }} + /> + {/each} +
+
+ + diff --git a/plugins/text-editor-resources/src/components/extension/table/TableNodeView.svelte b/plugins/text-editor-resources/src/components/extension/table/TableNodeView.svelte index fb164977d2..b4da3b4912 100644 --- a/plugins/text-editor-resources/src/components/extension/table/TableNodeView.svelte +++ b/plugins/text-editor-resources/src/components/extension/table/TableNodeView.svelte @@ -200,7 +200,7 @@ .table-toolbar-container { position: absolute; top: -1.5rem; - right: 0; + right: var(--table-offscreen-spacing); z-index: 200; } diff --git a/plugins/text-editor-resources/src/components/extension/table/TableToolbar.svelte b/plugins/text-editor-resources/src/components/extension/table/TableToolbar.svelte index b2fc07a074..406f778eda 100644 --- a/plugins/text-editor-resources/src/components/extension/table/TableToolbar.svelte +++ b/plugins/text-editor-resources/src/components/extension/table/TableToolbar.svelte @@ -50,15 +50,34 @@ actions = out } - $: actionsQuery.query(textEditor.class.TextEditorAction, { category: 70 }, (result) => { + $: actionsQuery.query(textEditor.class.TextEditorAction, { kind: 'table' }, (result) => { void updateActions([...result], actionCtx) }) + + $: categories = actions.reduce<[number, TextEditorAction][][]>((acc, action) => { + const { category, index } = action + if (acc[category] === undefined) acc[category] = [] + acc[category].push([index, action]) + return acc + }, []) + + $: categories.forEach((category) => { + category.sort((a, b) => a[0] - b[0]) + })
- {#each actions as action} - - {/each} +
+ {#each Object.values(categories) as category, index} + {#if index > 0} +
+ {/if} + + {#each category as [_, action]} + + {/each} + {/each} +