@kubex/zinc 1.1.100 → 1.1.102

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -157,6 +157,65 @@ outgrows the 20rem cap set here."
157
157
  ></zn-remarkd-editor>
158
158
  ```
159
159
 
160
+ ### Actions
161
+
162
+ The toolbar and the `/` slash menu are driven by one shared registry of actions, grouped
163
+ under ten headings: **Text**, **Inline**, **Lists**, **Admonitions**, **Blocks**,
164
+ **Structured**, **Media**, **Objects**, **Breaks**, and **Logic**. The toolbar shows as
165
+ many groups as fit its width and folds the rest into an overflow menu; the slash menu
166
+ carries every action regardless of width, and typing narrows it by label or keyword — so
167
+ a folded toolbar group is never actually out of reach.
168
+
169
+ {% raw %}
170
+
171
+ ```html:preview
172
+ <zn-remarkd-editor value='{{button:action text="Get started" href=/}}
173
+
174
+ {{video:dQw4w9WgXcQ source=youtube}}
175
+
176
+ {{reflist}}'></zn-remarkd-editor>
177
+ ```
178
+
179
+ {% endraw %}
180
+
181
+ ### Inline Formatting
182
+
183
+ Inline actions — Strong, Emphasis, Code, Underline, Strikethrough, Highlight, and the
184
+ rest of the Inline group — apply to the current selection inside the block being edited,
185
+ so they only make sense while a block is open: the toolbar disables them until one is
186
+ (the slash menu lists them regardless, since it can only ever open inside a block in the
187
+ first place). Choosing one from the toolbar wraps the selection in its mark, or — with
188
+ nothing selected — inserts a placeholder inside the mark with the placeholder selected so
189
+ typing replaces it; from the slash menu it always inserts the full construct with the
190
+ caret collapsed at the start of the placeholder, since there is no selection to wrap.
191
+ Choosing the same action again on a selection that already carries the mark unwraps it
192
+ rather than nesting a second one.
193
+
194
+ ```html:preview
195
+ <zn-remarkd-editor value="Click into this block, select a word, then choose Strong or Emphasis from the toolbar."></zn-remarkd-editor>
196
+ ```
197
+
198
+ ### Variables and Conditionals
199
+
200
+ The Logic group's actions insert document attributes (`:name: value`) and remarkd's
201
+ conditional directives (`ifdef`, `ifndef`, `ifeval`) — but the editor only ever *marks*
202
+ them, it never evaluates them: an attribute line renders as a chip, a `{name}` reference
203
+ in surrounding text renders as a token, and an `ifdef`/`ifndef`/`ifeval` range renders as
204
+ a labelled wrapper whose content stays visible regardless of whether the flag is defined.
205
+ Evaluating conditionals belongs to the renderer the finished document is published
206
+ through, not to this editor — showing both branches of an if/else here means neither one
207
+ ever disappears while you're still editing it.
208
+
209
+ ```html:preview
210
+ <zn-remarkd-editor value=":product: Remarkd
211
+
212
+ This documents {product}.
213
+
214
+ ifdef::beta[]
215
+ Only for the beta.
216
+ endif::[]"></zn-remarkd-editor>
217
+ ```
218
+
160
219
  ### Form Integration
161
220
 
162
221
  `zn-remarkd-editor` is a [form control](/getting-started/form-controls); its
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubex/zinc",
3
- "version": "1.1.100",
3
+ "version": "1.1.102",
4
4
  "description": "A collection of web components for building web applications based off of @shoelace-style/Shoelace",
5
5
  "keywords": [
6
6
  "web components",
@@ -8,8 +8,8 @@ import ZnIcon from "../icon";
8
8
  import ZnMenuItem from "../menu-item";
9
9
  import ZnTooltip from "../tooltip";
10
10
 
11
- import styles from './menu.scss';
12
11
  import { classMap } from "lit/directives/class-map.js";
12
+ import styles from './menu.scss';
13
13
 
14
14
  interface NavItem {
15
15
  title: string;
@@ -44,6 +44,8 @@ interface NavItem {
44
44
  * @csspart base - The component's base wrapper.
45
45
  *
46
46
  * @cssproperty --example - An example CSS custom property.
47
+ * @cssproperty --zn-menu-max-height - Caps the menu's height and makes it scroll internally.
48
+ * Unset (`none`) by default, so the menu grows to fit its items unless a consumer sets this.
47
49
  */
48
50
  export default class ZnMenu extends ZincElement {
49
51
  static styles: CSSResultGroup = unsafeCSS(styles);
@@ -101,6 +103,7 @@ export default class ZnMenu extends ZincElement {
101
103
  render() {
102
104
  return html`
103
105
  <div
106
+ part="base"
104
107
  class="${classMap({
105
108
  'menu': true,
106
109
  'menu--shell': this.variant === 'shell',
@@ -8,6 +8,9 @@
8
8
  border-radius: var(--zn-border-radius);
9
9
  width: fit-content;
10
10
  min-width: var(--zn-size-spanel);
11
+ // Unconstrained by default — a consumer that needs the menu to scroll inside a bounded
12
+ // popup (rather than growing to fit all its items) sets this custom property.
13
+ max-height: var(--zn-menu-max-height, none);
11
14
  overflow: auto;
12
15
  overscroll-behavior: none;
13
16
  box-shadow: 0 4px 8px -4px rgba(33, 33, 33, 0.1);
@@ -14,6 +14,10 @@
14
14
  --zn-page-texture-size: auto;
15
15
  --zn-page-content-background: var(--zn-body);
16
16
  --zn-page-header-background: var(--zn-page-surface-background, var(--zn-page-background, var(--zn-body)));
17
+ // A modal shell overlays its own close button at --zn-base-gap from the page edge;
18
+ // these describe it so the header can keep clear of it.
19
+ --zn-page-closer-size: 36px;
20
+ --zn-page-closer-gap: var(--zn-spacing-x-small);
17
21
  }
18
22
 
19
23
  :host([panel]) {
@@ -151,6 +155,7 @@
151
155
  justify-content: space-between;
152
156
  gap: 2px;
153
157
  max-width: 100%;
158
+ min-width: 0;
154
159
  min-height: 36px;
155
160
 
156
161
  a {
@@ -168,14 +173,14 @@
168
173
  gap: var(--zn-spacing-x-small);
169
174
  min-height: 36px;
170
175
  flex-shrink: 1;
176
+ min-width: 0;
171
177
  max-width: 100%;
172
178
  }
173
179
 
174
180
  &__right {
175
181
  flex-grow: 1;
176
- flex-shrink: 1;
182
+ flex-shrink: 0;
177
183
  overflow: hidden;
178
- min-width: 20px;
179
184
  }
180
185
 
181
186
  &__caption {
@@ -198,6 +203,7 @@
198
203
  &__description {
199
204
  @include wc.text-style(subheading);
200
205
  display: block;
206
+ overflow-wrap: anywhere;
201
207
  }
202
208
 
203
209
  .breadcrumb {
@@ -367,6 +373,12 @@ zn-navbar {
367
373
  padding-inline: calc(var(--zn-gutter, 0) + var(--zn-base-gap, 0));
368
374
  }
369
375
 
376
+ // The gutter shrinks to 0 on narrow containers, which would run the header actions
377
+ // under the closer. Reserving the closer's own footprint holds the gap steady instead.
378
+ :host([modal]) .header {
379
+ padding-inline-end: calc(var(--zn-base-gap, 0) + var(--zn-page-closer-size) + var(--zn-page-closer-gap));
380
+ }
381
+
370
382
  :host zn-navbar {
371
383
  &::part(navbar) {
372
384
  padding-inline: calc(var(--zn-gutter, 0) + var(--zn-base-gap, 0));
@@ -0,0 +1,159 @@
1
+ import type {SlashMenuItem} from '../slash-menu';
2
+
3
+ export type ActionGroup =
4
+ 'text' | 'lists' | 'admonitions' | 'blocks' | 'structured' | 'media' | 'objects' | 'breaks' | 'logic' | 'inline';
5
+
6
+ /** What an inline action wraps the selection in. `after` defaults to `before`. */
7
+ export interface InlineMark {
8
+ before: string;
9
+ after?: string;
10
+ placeholder?: string;
11
+ }
12
+
13
+ export interface EditorAction {
14
+ /** Stable id, used for toolbar keys and tests. */
15
+ key: string;
16
+ label: string;
17
+ icon: string;
18
+ group: ActionGroup;
19
+ /** Extra search terms for the slash menu, e.g. "bold" finds Strong. */
20
+ keywords?: string[];
21
+ /** Block actions: spliced in as a new block. */
22
+ prefix?: string;
23
+ /** Inline actions: applied to the selection in the open block. */
24
+ inline?: InlineMark;
25
+ /** Where the caret lands within `prefix`. Defaults to the end. */
26
+ caretOffset?: number;
27
+ /** Actions that open their own picker instead of inserting text. */
28
+ opens?: 'image' | 'include';
29
+ }
30
+
31
+ /** Toolbar order, most-used first — the last groups are the first to collapse. */
32
+ export const ACTION_GROUPS: { id: ActionGroup; label: string }[] = [
33
+ {id: 'text', label: 'Text'},
34
+ // Bold/italic/code are the most-reached actions in any editor — kept right after Text so
35
+ // they are among the last groups to collapse, not the first.
36
+ {id: 'inline', label: 'Inline'},
37
+ {id: 'lists', label: 'Lists'},
38
+ {id: 'admonitions', label: 'Admonitions'},
39
+ {id: 'blocks', label: 'Blocks'},
40
+ {id: 'structured', label: 'Structured'},
41
+ {id: 'media', label: 'Media'},
42
+ {id: 'objects', label: 'Objects'},
43
+ {id: 'breaks', label: 'Breaks'},
44
+ {id: 'logic', label: 'Logic'},
45
+ ];
46
+
47
+ export const EDITOR_ACTIONS: EditorAction[] = [
48
+ // Text
49
+ {key: 'text', label: 'Text', icon: 'text@lu', group: 'text', prefix: ''},
50
+ {key: 'paragraphs', label: 'Paragraph', icon: 'pilcrow@lu', group: 'text', prefix: ''},
51
+ {key: 'heading', label: 'Heading 1', icon: 'heading-1@lu', group: 'text', prefix: '# '},
52
+ {key: 'h2', label: 'Heading 2', icon: 'heading-2@lu', group: 'text', prefix: '## '},
53
+ {key: 'h3', label: 'Heading 3', icon: 'heading-3@lu', group: 'text', prefix: '### '},
54
+ {key: 'asciidoc-section', label: 'Section', icon: 'section@lu', group: 'text', prefix: '== '},
55
+ {key: 'attributes-title', label: 'Title', icon: 'captions@lu', group: 'text', prefix: '.'},
56
+
57
+ // Lists
58
+ {key: 'unordered-list', label: 'Bullet list', icon: 'list@lu', group: 'lists', prefix: '- ', keywords: ['bullet', 'ul']},
59
+ {key: 'ordered-list', label: 'Numbered list', icon: 'list-ordered@lu', group: 'lists', prefix: '1. ', keywords: ['ol']},
60
+ {key: 'checkboxes', label: 'Checklist', icon: 'list-todo@lu', group: 'lists', prefix: '- [ ] ', keywords: ['task', 'todo']},
61
+ {key: 'definition-list', label: 'Definition list', icon: 'book-a@lu', group: 'lists', prefix: 'Term:: Definition'},
62
+
63
+ // Admonitions
64
+ {key: 'admonition', label: 'Note', icon: 'info@lu', group: 'admonitions', prefix: 'NOTE: '},
65
+ {key: 'tip', label: 'Tip', icon: 'lightbulb@lu', group: 'admonitions', prefix: 'TIP: '},
66
+ {key: 'warning', label: 'Warning', icon: 'triangle-alert@lu', group: 'admonitions', prefix: 'WARNING: '},
67
+ {key: 'important', label: 'Important', icon: 'circle-alert@lu', group: 'admonitions', prefix: 'IMPORTANT: '},
68
+ {key: 'caution', label: 'Caution', icon: 'shield-alert@lu', group: 'admonitions', prefix: 'CAUTION: '},
69
+ {key: 'danger', label: 'Danger', icon: 'octagon-alert@lu', group: 'admonitions', prefix: 'DANGER: '},
70
+ {key: 'success', label: 'Success', icon: 'circle-check@lu', group: 'admonitions', prefix: 'SUCCESS: '},
71
+ {key: 'notice', label: 'Notice', icon: 'megaphone@lu', group: 'admonitions', prefix: 'NOTICE: '},
72
+
73
+ // Blocks — caretOffset lands inside the delimiters
74
+ {key: 'code-fence', label: 'Code', icon: 'code@lu', group: 'blocks', prefix: '```\n\n```', caretOffset: 4},
75
+ {key: 'blockquotes', label: 'Quote', icon: 'quote@lu', group: 'blocks', prefix: '____\n\n____', caretOffset: 5},
76
+ {key: 'verse', label: 'Verse', icon: 'feather@lu', group: 'blocks', prefix: '[verse]\n____\n\n____', caretOffset: 13},
77
+ {key: 'sidebar-block', label: 'Sidebar', icon: 'panel-right@lu', group: 'blocks', prefix: '****\n\n****', caretOffset: 5},
78
+ {key: 'example-block', label: 'Example', icon: 'square-dashed@lu', group: 'blocks', prefix: '====\n\n====', caretOffset: 5},
79
+ {key: 'literal-block', label: 'Literal', icon: 'file-code@lu', group: 'blocks', prefix: '....\n\n....', caretOffset: 5},
80
+ {key: 'listing-block', label: 'Listing', icon: 'terminal@lu', group: 'blocks', prefix: '----\n\n----', caretOffset: 5},
81
+ {key: 'generic-container', label: 'Container', icon: 'box@lu', group: 'blocks', prefix: '!!!!\n\n!!!!', caretOffset: 5},
82
+ {key: 'comments', label: 'Comment', icon: 'message-square-dashed@lu', group: 'blocks', prefix: '////\n\n////', caretOffset: 5},
83
+ {key: 'hardbreaks', label: 'Hard breaks', icon: 'wrap-text@lu', group: 'blocks', prefix: '[%hardbreaks]\n', keywords: ['line breaks']},
84
+
85
+ // Structured
86
+ {key: 'tabs', label: 'Tabs', icon: 'app-window@lu', group: 'structured', prefix: '_|_#first [name=First]\nFirst content', caretOffset: 16},
87
+ {key: 'steps', label: 'Steps', icon: 'list-checks@lu', group: 'structured', prefix: '_|- First step\nDo first', caretOffset: 4},
88
+ {key: 'callout-block', label: 'Callout', icon: 'message-square-code@lu', group: 'structured', prefix: '<1> '},
89
+
90
+ // Media
91
+ {key: 'asciidoc-image', label: 'Image', icon: 'image@lu', group: 'media', opens: 'image'},
92
+ {key: 'image-alignment', label: 'Aligned image', icon: 'image-plus@lu', group: 'media', prefix: '[.align-center]\nimage::file.png[Alt,640,480]', caretOffset: 23},
93
+ {key: 'video', label: 'Video', icon: 'video@lu', group: 'media', prefix: '{{video:ID source=youtube}}', caretOffset: 8},
94
+ {key: 'include', label: 'Include', icon: 'blocks@lu', group: 'media', opens: 'include'},
95
+ // The path placeholder is load-bearing: `t::partial::` with an empty path makes parse() throw.
96
+ {key: 'partial', label: 'Partial', icon: 'file-stack@lu', group: 'media', prefix: 't::partial::path/to/file.remarkd', caretOffset: 12},
97
+ {key: 'reference-list', label: 'Reference list', icon: 'book-marked@lu', group: 'media', prefix: '{{reflist}}'},
98
+
99
+ // Objects
100
+ {key: 'button-object', label: 'Button', icon: 'mouse-pointer-click@lu', group: 'objects', prefix: '{{button:action text="Label" href=/}}', caretOffset: 9},
101
+ {key: 'object-macros', label: 'Link object', icon: 'external-link@lu', group: 'objects', prefix: '{{link:https:// text=Label}}', caretOffset: 7},
102
+ {key: 'anchor', label: 'Anchor', icon: 'anchor@lu', group: 'objects', prefix: '{{anchor name=top}}', caretOffset: 14},
103
+ {key: 'meter', label: 'Meter', icon: 'gauge@lu', group: 'objects', prefix: '{{meter id=progress min=0 max=10 value=5}}', caretOffset: 11},
104
+ {key: 'object-attributes', label: 'Break object', icon: 'separator-horizontal@lu', group: 'objects', prefix: '{{br}}'},
105
+
106
+ // Breaks
107
+ {key: 'horizontal-rule', label: 'Divider', icon: 'minus@lu', group: 'breaks', prefix: '---', keywords: ['rule', 'hr']},
108
+ {key: 'page-break', label: 'Page break', icon: 'scissors-line-dashed@lu', group: 'breaks', prefix: '<<<'},
109
+ {key: 'line-continuation', label: 'Line continuation', icon: 'corner-down-left@lu', group: 'breaks', prefix: ' \\'},
110
+
111
+ // Logic — marked in the preview, never evaluated
112
+ {key: 'document-attributes', label: 'Variable', icon: 'variable@lu', group: 'logic', prefix: ':name: value', caretOffset: 1, keywords: ['attribute', 'variable']},
113
+ {key: 'conditionals', label: 'If defined', icon: 'git-branch@lu', group: 'logic', prefix: 'ifdef::flag[]\n\nendif::[]', caretOffset: 7},
114
+ {key: 'ifndef', label: 'If not defined', icon: 'git-branch-minus@lu', group: 'logic', prefix: 'ifndef::flag[]\n\nendif::[]', caretOffset: 8},
115
+ {key: 'iftrue', label: 'If true', icon: 'toggle-right@lu', group: 'logic', prefix: 'iftrue::flag[Shown]', caretOffset: 8},
116
+ {key: 'iffalse', label: 'If false', icon: 'toggle-left@lu', group: 'logic', prefix: 'iffalse::flag[Shown]', caretOffset: 9},
117
+ {key: 'ifempty', label: 'If empty', icon: 'square@lu', group: 'logic', prefix: 'ifempty::flag[Shown]', caretOffset: 9},
118
+ {key: 'ifnempty', label: 'If not empty', icon: 'square-check@lu', group: 'logic', prefix: 'ifnempty::flag[Shown]', caretOffset: 10},
119
+ {key: 'ifeval', label: 'If expression', icon: 'equal@lu', group: 'logic', prefix: 'ifeval::[1 > 0]\n\nendif::[]', caretOffset: 9},
120
+
121
+ // Inline — applied to the selection in the open block
122
+ {key: 'inline-formatting', label: 'Strong', icon: 'bold@lu', group: 'inline', keywords: ['bold'], inline: {before: '**', placeholder: 'text'}},
123
+ {key: 'emphasis', label: 'Emphasis', icon: 'italic@lu', group: 'inline', keywords: ['italic'], inline: {before: '__', placeholder: 'text'}},
124
+ {key: 'inline-code', label: 'Code', icon: 'square-code@lu', group: 'inline', inline: {before: '`', placeholder: 'code'}},
125
+ {key: 'underline', label: 'Underline', icon: 'underline@lu', group: 'inline', inline: {before: '___', placeholder: 'text'}},
126
+ {key: 'strike', label: 'Strikethrough', icon: 'strikethrough@lu', group: 'inline', keywords: ['delete'], inline: {before: '~~', placeholder: 'text'}},
127
+ {key: 'highlight', label: 'Highlight', icon: 'highlighter@lu', group: 'inline', keywords: ['mark'], inline: {before: '#', placeholder: 'text'}},
128
+ {key: 'subscript', label: 'Subscript', icon: 'subscript@lu', group: 'inline', inline: {before: '~', placeholder: '2'}},
129
+ {key: 'superscript', label: 'Superscript', icon: 'superscript@lu', group: 'inline', inline: {before: '^', placeholder: '2'}},
130
+ {key: 'keyboard', label: 'Keyboard', icon: 'keyboard@lu', group: 'inline', keywords: ['kbd', 'shortcut'], inline: {before: 'kbd:[', after: ']', placeholder: 'Ctrl+C'}},
131
+ {key: 'footnote', label: 'Footnote', icon: 'asterisk@lu', group: 'inline', inline: {before: 'footnote:[', after: ']', placeholder: 'Note'}},
132
+ {key: 'tooltip', label: 'Tooltip', icon: 'message-circle-question-mark@lu', group: 'inline', keywords: ['term'], inline: {before: '{', after: '}(Explanation)', placeholder: 'Term'}},
133
+ {key: 'cross-reference', label: 'Cross reference', icon: 'link-2@lu', group: 'inline', keywords: ['xref'], inline: {before: '<<', after: '>>', placeholder: 'section,Label'}},
134
+ {key: 'links-and-images', label: 'Link', icon: 'link@lu', group: 'inline', inline: {before: '[', after: '](https://)', placeholder: 'Label'}},
135
+ {key: 'passthrough', label: 'Passthrough', icon: 'shield@lu', group: 'inline', keywords: ['raw', 'literal'], inline: {before: 'pass:[', after: ']', placeholder: 'raw'}},
136
+ {key: 'curly-bang-passthrough', label: 'Literal braces', icon: 'braces@lu', group: 'inline', inline: {before: '{!', after: '!}', placeholder: 'raw'}},
137
+ ];
138
+
139
+ /** The registry as slash menu entries. Picker actions insert nothing; the menu emits their action id. */
140
+ export function slashItems(actions: EditorAction[]): SlashMenuItem[] {
141
+ const label = (id: ActionGroup) => ACTION_GROUPS.find(group => group.id === id)?.label;
142
+ return actions.map(action => {
143
+ if (action.opens) {
144
+ return {label: action.label, icon: action.icon, action: action.opens, group: label(action.group), keywords: action.keywords};
145
+ }
146
+ // Inline actions have no textarea selection to wrap from the slash menu, so insert the
147
+ // whole construct with the caret at the placeholder — a bare `before` would leave the
148
+ // mark unclosed (`**` with nothing selected to wrap).
149
+ const {before, after = before, placeholder = ''} = action.inline ?? {before: action.prefix ?? ''};
150
+ return {
151
+ label: action.label,
152
+ icon: action.icon,
153
+ group: label(action.group),
154
+ keywords: action.keywords,
155
+ value: action.inline ? before + placeholder + after : before,
156
+ caretOffset: action.inline ? before.length : action.caretOffset,
157
+ };
158
+ });
159
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The remarkd feature fixtures, from `node_modules/remarkd-js/requirements/features/`.
3
+ * Regenerate with: ls node_modules/remarkd-js/requirements/features
4
+ */
5
+ export const FEATURE_KEYS: string[] = [
6
+ 'accordion', 'admonition', 'asciidoc-image', 'asciidoc-section', 'attributes-title', 'autolink',
7
+ 'blockquotes', 'button-object', 'callout-block', 'checkboxes', 'code-fence', 'comments',
8
+ 'conditionals', 'conditionals-advanced', 'cross-reference', 'curly-bang-passthrough',
9
+ 'definition-list', 'document-attributes', 'emoji-aliases', 'example-block', 'footnote',
10
+ 'generic-container', 'hardbreaks', 'heading', 'highlight', 'horizontal-rule', 'id-block',
11
+ 'image-alignment', 'include', 'inline-advanced', 'inline-formatting', 'keyboard',
12
+ 'line-continuation', 'links-and-images', 'listing-block', 'literal-block', 'object-attributes',
13
+ 'object-fallbacks', 'object-macros', 'ordered-list', 'page-break', 'paragraphs', 'partial',
14
+ 'passthrough', 'pros-cons', 'reference-list', 'sidebar-block', 'smart-quotes', 'steps',
15
+ 'subscript', 'superscript', 'table', 'tabs', 'tooltip', 'typographic-symbols',
16
+ 'unordered-list', 'url-formatting-chars', 'verse', 'video',
17
+ ];