@sveltia/ui 0.73.1 → 0.75.0

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.
@@ -1,21 +1,33 @@
1
1
  <!--
2
2
  @component
3
- The interactive version of `<TableBody>`.
3
+ The interactive version of `<TableBody>`. A labelled row group can be made collapsible, in which
4
+ case the caption doubles as an expander button that shows or hides the rows.
4
5
  @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/tbody
5
6
  @see https://w3c.github.io/aria/#rowgroup
7
+ @see https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/
6
8
  -->
7
9
  <script>
8
- import { _ } from '@sveltia/i18n';
10
+ import { _, isRTL } from '@sveltia/i18n';
11
+ import Button from '../button/button.svelte';
12
+ import Icon from '../icon/icon.svelte';
9
13
 
10
14
  /**
11
15
  * @import { Snippet } from 'svelte';
16
+ * @import { Attachment } from 'svelte/attachments';
12
17
  */
13
18
 
14
19
  /**
15
20
  * @typedef {object} Props
16
21
  * @property {string} [class] The `class` attribute on the wrapper element.
17
22
  * @property {string} [label] Display label for the row group.
23
+ * @property {boolean} [collapsible] Whether the rows can be hidden with an expander in the
24
+ * caption. Requires `label`.
25
+ * @property {boolean} [expanded] Whether the rows are shown. Only applies when `collapsible`. An
26
+ * alias of the `aria-expanded` attribute on the expander button.
18
27
  * @property {Snippet} [children] Primary slot content.
28
+ * @property {Snippet} [chevronIcon] Chevron slot content for the expander.
29
+ * @property {(event: CustomEvent) => void} [onChange] Custom `Change` event handler, called with
30
+ * the new `expanded` state in `detail` when the expander is toggled.
19
31
  */
20
32
 
21
33
  /**
@@ -23,14 +35,52 @@
23
35
  */
24
36
  let {
25
37
  /* eslint-disable prefer-const */
38
+ expanded = $bindable(true),
26
39
  class: className,
27
40
  label = '',
41
+ collapsible = false,
28
42
  children,
43
+ chevronIcon,
44
+ onChange,
29
45
  ...restProps
30
46
  /* eslint-enable prefer-const */
31
47
  } = $props();
32
48
 
33
49
  const id = $props.id();
50
+ const hasExpander = $derived(collapsible && !!label);
51
+
52
+ /**
53
+ * Toggle the `expanded` state and notify the consumer.
54
+ */
55
+ const toggle = () => {
56
+ expanded = !expanded;
57
+ onChange?.(new CustomEvent('change', { detail: { expanded } }));
58
+ };
59
+
60
+ /**
61
+ * Toggle the expander when the caption row itself is clicked. The `Grid` treats the caption as a
62
+ * row: the arrow keys land on it, and Enter clicks it, so this is how a keyboard user who has
63
+ * moved to the caption toggles the group. A click bubbling up from the button is left alone, as
64
+ * the button has toggled the group already.
65
+ * @type {Attachment}
66
+ */
67
+ const handleCaptionClick = (element) => {
68
+ /**
69
+ * Handle the `click` event on the caption row.
70
+ * @param {Event} event `click` event.
71
+ */
72
+ const listener = (event) => {
73
+ if (hasExpander && event.target === element) {
74
+ toggle();
75
+ }
76
+ };
77
+
78
+ element.addEventListener('click', listener);
79
+
80
+ return () => {
81
+ element.removeEventListener('click', listener);
82
+ };
83
+ };
34
84
  </script>
35
85
 
36
86
  <div
@@ -41,14 +91,34 @@
41
91
  aria-roledescription={_('_sui.role_descriptions.grid_body')}
42
92
  >
43
93
  {#if label}
44
- <div role="row" class="row-group-caption">
94
+ <div role="row" class="row-group-caption" {@attach handleCaptionClick}>
45
95
  <!-- We need `colspan` here but cannot place `<th>` under `<div>`, so use a hack -->
46
96
  <svelte:element this={"th"} role="columnheader" id="{id}-label" colspan="9999">
47
- {label}
97
+ {#if hasExpander}
98
+ <!--
99
+ The button is not a tab stop: the grid has a single one, and a keyboard user reaches
100
+ the caption row with the arrow keys and toggles it with Enter
101
+ -->
102
+ <Button class="expander" {label} tabindex="-1" aria-expanded={expanded} onclick={toggle}>
103
+ {#snippet startIcon()}
104
+ <span role="none" class="chevron">
105
+ {#if chevronIcon}
106
+ {@render chevronIcon()}
107
+ {:else}
108
+ <Icon name={isRTL() ? 'chevron_left' : 'chevron_right'} />
109
+ {/if}
110
+ </span>
111
+ {/snippet}
112
+ </Button>
113
+ {:else}
114
+ {label}
115
+ {/if}
48
116
  </svelte:element>
49
117
  </div>
50
118
  {/if}
51
- {@render children?.()}
119
+ {#if !hasExpander || expanded}
120
+ {@render children?.()}
121
+ {/if}
52
122
  </div>
53
123
 
54
124
  <style>[role=rowgroup] {
@@ -66,4 +136,32 @@
66
136
  background-color: var(--sui-secondary-background-color);
67
137
  font-size: var(--sui-font-size-default);
68
138
  text-align: start;
139
+ }
140
+ [role=columnheader]:has(> :global(.expander)) {
141
+ padding: 0;
142
+ }
143
+ [role=columnheader] :global(.expander) {
144
+ justify-content: flex-start;
145
+ margin: 0;
146
+ border-radius: 0;
147
+ padding: 4px 8px 4px 4px;
148
+ width: 100%;
149
+ height: auto;
150
+ color: inherit;
151
+ font-size: inherit;
152
+ text-align: start;
153
+ }
154
+
155
+ .chevron {
156
+ flex: none;
157
+ display: flex;
158
+ align-items: center;
159
+ justify-content: center;
160
+ transition: transform 200ms;
161
+ }
162
+ :global(.expander[aria-expanded='true']) > .chevron:dir(ltr) {
163
+ transform: rotate(90deg);
164
+ }
165
+ :global(.expander[aria-expanded='true']) > .chevron:dir(rtl) {
166
+ transform: rotate(-90deg);
69
167
  }</style>
@@ -4,9 +4,11 @@ type GridBody = {
4
4
  $set?(props: Partial<Props & Record<string, any>>): void;
5
5
  };
6
6
  /**
7
- * The interactive version of `<TableBody>`.
7
+ * The interactive version of `<TableBody>`. A labelled row group can be made collapsible, in which
8
+ * case the caption doubles as an expander button that shows or hides the rows.
8
9
  * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/tbody
9
10
  * @see https://w3c.github.io/aria/#rowgroup
11
+ * @see https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/
10
12
  */
11
13
  declare const GridBody: import("svelte").Component<{
12
14
  /**
@@ -17,11 +19,30 @@ declare const GridBody: import("svelte").Component<{
17
19
  * Display label for the row group.
18
20
  */
19
21
  label?: string | undefined;
22
+ /**
23
+ * Whether the rows can be hidden with an expander in the
24
+ * caption. Requires `label`.
25
+ */
26
+ collapsible?: boolean | undefined;
27
+ /**
28
+ * Whether the rows are shown. Only applies when `collapsible`. An
29
+ * alias of the `aria-expanded` attribute on the expander button.
30
+ */
31
+ expanded?: boolean | undefined;
20
32
  /**
21
33
  * Primary slot content.
22
34
  */
23
35
  children?: Snippet<[]> | undefined;
24
- } & Record<string, any>, {}, "">;
36
+ /**
37
+ * Chevron slot content for the expander.
38
+ */
39
+ chevronIcon?: Snippet<[]> | undefined;
40
+ /**
41
+ * Custom `Change` event handler, called with
42
+ * the new `expanded` state in `detail` when the expander is toggled.
43
+ */
44
+ onChange?: ((event: CustomEvent) => void) | undefined;
45
+ } & Record<string, any>, {}, "expanded">;
25
46
  type Props = {
26
47
  /**
27
48
  * The `class` attribute on the wrapper element.
@@ -31,9 +52,28 @@ type Props = {
31
52
  * Display label for the row group.
32
53
  */
33
54
  label?: string | undefined;
55
+ /**
56
+ * Whether the rows can be hidden with an expander in the
57
+ * caption. Requires `label`.
58
+ */
59
+ collapsible?: boolean | undefined;
60
+ /**
61
+ * Whether the rows are shown. Only applies when `collapsible`. An
62
+ * alias of the `aria-expanded` attribute on the expander button.
63
+ */
64
+ expanded?: boolean | undefined;
34
65
  /**
35
66
  * Primary slot content.
36
67
  */
37
68
  children?: Snippet<[]> | undefined;
69
+ /**
70
+ * Chevron slot content for the expander.
71
+ */
72
+ chevronIcon?: Snippet<[]> | undefined;
73
+ /**
74
+ * Custom `Change` event handler, called with
75
+ * the new `expanded` state in `detail` when the expander is toggled.
76
+ */
77
+ onChange?: ((event: CustomEvent) => void) | undefined;
38
78
  };
39
79
  import type { Snippet } from 'svelte';
@@ -92,28 +92,14 @@
92
92
  let keyResizing = $state(false);
93
93
 
94
94
  /**
95
- * The pane group container's size in pixels, used for px→% conversion. Measured once when a drag
96
- * begins rather than on every pointer move: reading `clientWidth` forces the browser to lay the
97
- * page out, which is the last thing wanted in the middle of a drag, and the container cannot
98
- * change size while the pointer holding it is down.
95
+ * The space the panes share in pixels, used for px→% conversion. Measured once when a drag
96
+ * begins rather than on every pointer move: measuring forces the browser to lay the page out,
97
+ * which is the last thing wanted in the middle of a drag, and the group cannot change size while
98
+ * the pointer holding it is down.
99
99
  * @type {number}
100
100
  */
101
101
  let containerSize = 0;
102
102
 
103
- /**
104
- * Measure the pane group container element's size in pixels.
105
- * @returns {number} Container size in pixels.
106
- */
107
- const measureContainerSize = () => {
108
- const container = element?.closest('.resizable-pane-group');
109
-
110
- // The handle can’t be rendered outside a group; see the context check above
111
- /* v8 ignore next */
112
- if (!container) return 0;
113
-
114
- return isHorizontal ? container.clientWidth : container.clientHeight;
115
- };
116
-
117
103
  /**
118
104
  * Handle pointer move events (attached to `document` while dragging).
119
105
  * @param {PointerEvent} event `pointermove` event.
@@ -180,7 +166,7 @@
180
166
  dragging = true;
181
167
  startScreenPos = isHorizontal ? screenX : screenY;
182
168
  targetPointerId = pointerId;
183
- containerSize = measureContainerSize();
169
+ containerSize = ctx.measurePaneSpace();
184
170
  element?.setPointerCapture(pointerId);
185
171
 
186
172
  onResizeStart?.();
@@ -73,16 +73,40 @@
73
73
 
74
74
  let _handleCount = 0;
75
75
 
76
+ /**
77
+ * Measure the space the panes share: the group’s size along its direction, less the handles
78
+ * between them. Every pane size is a percentage of this, so the panes and handles together fill
79
+ * the group exactly. Were the percentages of the whole group, the handles would push the last
80
+ * pane past the edge; the overflow is hidden, but the group could still be scrolled by that much
81
+ * whenever something inside is scrolled into view, and everything would shift out of place.
82
+ * @returns {number} Space in pixels, or `0` if the group isn’t laid out.
83
+ */
84
+ const measurePaneSpace = () => {
85
+ if (!element) {
86
+ return 0;
87
+ }
88
+
89
+ const isHorizontal = direction === 'horizontal';
90
+ const groupSize = isHorizontal ? element.clientWidth : element.clientHeight;
91
+
92
+ const handleSize = [...element.querySelectorAll(':scope > .resizable-handle')].reduce(
93
+ (total, handle) =>
94
+ total +
95
+ (isHorizontal
96
+ ? /** @type {HTMLElement} */ (handle).offsetWidth
97
+ : /** @type {HTMLElement} */ (handle).offsetHeight),
98
+ 0,
99
+ );
100
+
101
+ return Math.max(groupSize - handleSize, 0);
102
+ };
103
+
76
104
  /**
77
105
  * Get the sizes the pane lengths are relative to.
78
106
  * @returns {import('./sizing.js').SizeEnvironment} Environment.
79
107
  */
80
108
  const getSizeEnvironment = () => ({
81
- containerSize: element
82
- ? direction === 'horizontal'
83
- ? element.clientWidth
84
- : element.clientHeight
85
- : 0,
109
+ containerSize: measurePaneSpace(),
86
110
  viewportWidth: window.innerWidth,
87
111
  viewportHeight: window.innerHeight,
88
112
  });
@@ -186,6 +210,7 @@
186
210
  resize,
187
211
  toggleCollapse,
188
212
  getPaneConstraints,
213
+ measurePaneSpace,
189
214
  paneDefs: _paneDefs,
190
215
  }),
191
216
  /* eslint-enable jsdoc/require-jsdoc */
@@ -13,9 +13,10 @@
13
13
 
14
14
  /**
15
15
  * @typedef {object} Props
16
- * @property {number | string} [defaultSize] Default size. Numbers are percentages (0-100);
17
- * strings can be any CSS length/percentage such as `240px`, `20%` or `20dvw`. Panes without
18
- * `defaultSize` share the remaining space equally.
16
+ * @property {number | string} [defaultSize] Default size. Numbers are percentages (0-100) of
17
+ * the space the panes share, which is the group less its handles; strings can be any CSS
18
+ * length/percentage such as `240px`, `20%` or `20dvw`. Panes without `defaultSize` share the
19
+ * remaining space equally.
19
20
  * @property {number | string} [minSize] Minimum size. Numbers are percentages, strings are CSS
20
21
  * lengths. Defaults to `0`.
21
22
  * @property {number | string} [maxSize] Maximum size. Numbers are percentages, strings are CSS
@@ -55,6 +56,12 @@
55
56
 
56
57
  const direction = $derived(ctx.direction);
57
58
  const size = $derived(ctx.sizes[paneIndex]);
59
+ /**
60
+ * The size as a flex basis. The percentages are of the space the panes share rather than of the
61
+ * group, while a flex basis is a percentage of the group, so the panes are made shrinkable: the
62
+ * handles then take their room from the panes in proportion to their sizes, which works out to
63
+ * exactly the intended percentages, and the group is never overflowed.
64
+ */
58
65
  const sizeStyle = $derived.by(() => {
59
66
  if (size !== undefined) {
60
67
  return `${size}%`;
@@ -81,7 +88,7 @@
81
88
  class="sui resizable-pane {className}"
82
89
  style:flex-basis={sizeStyle}
83
90
  style:flex-grow="0"
84
- style:flex-shrink="0"
91
+ style:flex-shrink="1"
85
92
  style:overflow-x={direction === 'horizontal' ? 'auto' : undefined}
86
93
  style:overflow-y={direction === 'vertical' ? 'auto' : undefined}
87
94
  >
@@ -9,9 +9,10 @@ type ResizablePane = {
9
9
  */
10
10
  declare const ResizablePane: import("svelte").Component<{
11
11
  /**
12
- * Default size. Numbers are percentages (0-100);
13
- * strings can be any CSS length/percentage such as `240px`, `20%` or `20dvw`. Panes without
14
- * `defaultSize` share the remaining space equally.
12
+ * Default size. Numbers are percentages (0-100) of
13
+ * the space the panes share, which is the group less its handles; strings can be any CSS
14
+ * length/percentage such as `240px`, `20%` or `20dvw`. Panes without `defaultSize` share the
15
+ * remaining space equally.
15
16
  */
16
17
  defaultSize?: string | number | undefined;
17
18
  /**
@@ -42,9 +43,10 @@ declare const ResizablePane: import("svelte").Component<{
42
43
  } & Record<string, any>, {}, "">;
43
44
  type Props = {
44
45
  /**
45
- * Default size. Numbers are percentages (0-100);
46
- * strings can be any CSS length/percentage such as `240px`, `20%` or `20dvw`. Panes without
47
- * `defaultSize` share the remaining space equally.
46
+ * Default size. Numbers are percentages (0-100) of
47
+ * the space the panes share, which is the group less its handles; strings can be any CSS
48
+ * length/percentage such as `240px`, `20%` or `20dvw`. Panes without `defaultSize` share the
49
+ * remaining space equally.
48
50
  */
49
51
  defaultSize?: string | number | undefined;
50
52
  /**
@@ -1,4 +1,4 @@
1
1
  /**
2
2
  * Version of this package, used to resolve the prebuilt Shiki engine chunk from a CDN.
3
3
  */
4
- export const UI_VERSION: "0.73.1";
4
+ export const UI_VERSION: "0.75.0";
@@ -3,4 +3,4 @@
3
3
  /**
4
4
  * Version of this package, used to resolve the prebuilt Shiki engine chunk from a CDN.
5
5
  */
6
- export const UI_VERSION = '0.73.1';
6
+ export const UI_VERSION = '0.75.0';
@@ -0,0 +1,141 @@
1
+ # --- NOTES ---
2
+ # 1. Prefer verbal nouns (like סגירה) over imperatives (like סגור)
3
+ # 2. Prefer neologisms (like סמלון) over loanwords (like אימוג'י)
4
+ # 3. Add a hyphen between Hebrew prefixes and English words (like ב-Markdown)
5
+
6
+ # Dialog action button labels
7
+ ok: אישור
8
+ cancel: ביטול
9
+ close: סגירה
10
+
11
+ # Button to clear content, e.g. search input or calendar date
12
+ clear: ניקוי
13
+
14
+ # Text editor dialog buttons for link/image insertion
15
+ insert: הוספה
16
+ update: עדכון
17
+ remove: הסרה
18
+
19
+ # Combobox dropdown toggle button aria-labels
20
+ collapse: צמצום
21
+ expand: הרחבה
22
+
23
+ # Infobar/toast dismiss button label
24
+ dismiss: סגירה
25
+
26
+ # Alert & infobar status, read out by screen readers before the message (visually hidden)
27
+ alert:
28
+ error: שגיאה
29
+ warning: אזהרה
30
+ info: מידע
31
+ success: הצלחה
32
+
33
+ # Role descriptions, read out by screen readers in place of the generic “group” role
34
+ role_descriptions:
35
+ disclosure: קבוצת גילוי
36
+ option_group: קבוצת אפשרויות
37
+ menu_item_group: קבוצת פריטי תפריט
38
+ checkbox_group: קבוצת תיבות סימון
39
+ table_head: כותרת טבלה
40
+ table_body: גוף טבלה
41
+ table_foot: תחתית טבלה
42
+ grid_head: כותרת רשת
43
+ grid_body: גוף רשת
44
+ grid_foot: תחתית רשת
45
+
46
+ # Emoji suggestion listbox aria-label, shared by the text fields and the text editor
47
+ emoji_suggestions: הצעות סמלונים
48
+
49
+ # Calendar component
50
+ calendar:
51
+ # View switcher group aria-labels and navigation button aria-labels
52
+ year: שנה
53
+ previous_decade: העשור הקודם
54
+ next_decade: העשור הבא
55
+ month: חודש
56
+ previous_month: החודש הקודם
57
+ next_month: החודש הבא
58
+ # “Today” button label in footer
59
+ today: היום
60
+
61
+ # Split button component
62
+ split_button:
63
+ # Wrapper aria-label, e.g. “Save Options” when primary button is “Save”
64
+ x_options: אפשרויות {$name}
65
+ # Dropdown toggle aria-label
66
+ more_options: אפשרויות נוספות
67
+
68
+ # Combobox component
69
+ combobox:
70
+ # Placeholder when no option is selected
71
+ select_an_option: בחירת אפשרות…
72
+ # Filter input aria-label
73
+ filter_options: סינון
74
+ # Empty state message when filter has no matches
75
+ no_matching_options: לא נמצאו אפשרויות תואמות
76
+
77
+ # Number input component
78
+ number_input:
79
+ # Spin button aria-labels for incrementing/decrementing the number value
80
+ increase: הגדלה
81
+ decrease: הקטנה
82
+
83
+ # Password input component
84
+ password_input:
85
+ # Visibility toggle button aria-labels
86
+ show_password: הצגת סיסמה
87
+ hide_password: הסתרת סיסמה
88
+
89
+ # Secret input component (API keys, tokens, etc.)
90
+ secret_input:
91
+ # Visibility toggle button aria-labels
92
+ show_secret: הצגת מפתח סודי
93
+ hide_secret: הסתרת מפתח סודי
94
+
95
+ # Select tags component (multi-select)
96
+ select_tags:
97
+ # Listbox aria-label
98
+ selected_options: אפשרויות שנבחרו
99
+ # Remove button aria-label, e.g. “Remove Option 1”
100
+ remove_x: הסרת {$name}
101
+
102
+ # Text editor component
103
+ text_editor:
104
+ # Toolbar aria-labels
105
+ text_editor: עורך טקסט
106
+ code_editor: עורך קוד
107
+ # Block style menu button and menu aria-labels
108
+ text_style_options: אפשרויות עיצוב טקסט
109
+ show_text_style_options: הצגת אפשרויות עיצוב טקסט
110
+ # Block style menu items
111
+ paragraph: פסקה
112
+ heading_1: כותרת 1
113
+ heading_2: כותרת 2
114
+ heading_3: כותרת 3
115
+ heading_4: כותרת 4
116
+ heading_5: כותרת 5
117
+ heading_6: כותרת 6
118
+ bulleted_list: רשימת תבליטים
119
+ numbered_list: רשימה ממוספרת
120
+ blockquote: ציטוט
121
+ code_block: קטע קוד
122
+ # Inline style button aria-labels
123
+ bold: הדגשה
124
+ italic: הטיה
125
+ strikethrough: קו חוצה
126
+ code: קוד
127
+ link: קישור
128
+ # Link dialog title
129
+ insert_link: הוספת קישור
130
+ update_link: עדכון קישור
131
+ # Link dialog input field labels
132
+ text: טקסט
133
+ url: כתובת קישור
134
+ # Markdown mode toggle button aria-label
135
+ edit_in_markdown: עריכה ב-Markdown
136
+ # Error message when rich text conversion fails
137
+ converter_error: לא ניתן להפעיל מצב טקסט עשיר. יש להשתמש בעורך טקסט פשוט במקום.
138
+ # Language selector aria-label
139
+ language: שפה
140
+ # Plain text mode option in language selector
141
+ plain_text: טקסט פשוט