@synopackageland/ui 0.11.0 → 0.11.1

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.11.1 (2026-10-17)
4
+
5
+ - Fill-height pages: `AppLayout height="fill"` takes its container's height (a DSM window) and scrolls its content area instead of the document; `PageContent height="fill"` keeps the page header, tabs and toolbar in place and lets its last child (a `DataGrid`, or a flex column holding one) take the height left. The grid scrolls its rows under its sticky header and its pager stays in view, so a 50-row page no longer pushes the pager far below. Both default to `height="auto"` (unchanged). The SDK's `AppShell` page `fill: true` does the same.
6
+ - `DataGrid` column `priority`: when the shown columns do not fit at their minimum widths, the grid leaves out the lowest priority first (the rightmost of a tie first), so an 800 px DSM window drops its least needed columns instead of scrolling sideways. Columns without a `priority`, or with `hideable: false`, always show. A column left out for width is not saved as hidden, is still searched by the quick filter, returns when the window widens, and is listed unchecked in the column chooser with "Too narrow" (new label `hiddenByWidth`, nine locales); checking it there shows it for this view.
7
+ - New `formatDateTime(value, { locale, style, timeZone, now, unknown })`. `style: 'compact'` fits a 140 px column in every built-in locale: this year `10/17 14:32` (zh-TW, zh-CN, ja), `10. 17. 14:32` (ko), `Oct 17, 2:32 PM` (en), `17. Okt., 14:32` (de); another year the date with its year (`2025/3/2`, `Mar 2, 2025`). The default `full` style (`Oct 17, 2026, 2:32:05 PM`, `2026年10月17日 下午2:32:05`) is for the tooltip. `DateTimeFormatOptions` is exported.
8
+ - **Visible change:** a running `CellStatus` fits one DataGrid row: the label line is 16 px, the gap 2 px and the bar a 4 px line (new `Progress size="sm"`), 22 px in all, so rows with a progress bar are as high as the others. A `detail` still adds a line.
9
+ - Every DataGrid body cell, a `#cell-<key>` slot's included, carries `data-column="<key>"`.
10
+ - Checkbox menu entries take a `hint`, a quiet note after the label. The column chooser keeps its menu open while columns are toggled (documented; unchanged).
11
+ - Visible changes for apps: Download Hub should mark its downloads page `fill: true` in `AppShell` and make the list's wrapper a flex column (`display:flex; flex-direction:column; min-height:0`), give optional columns a `priority` (start time 1, speed 2, location 3, size 4), format times with `formatDateTime(..., { style: 'compact' })` with the full form as `title` instead of its own `Intl.DateTimeFormat`, and drop its own `data-cell` spans used only to find cells in tests.
12
+
13
+ 繁中摘要:新增填滿高度的頁面:`AppLayout height="fill"` 取容器高度(DSM 視窗)、由內容區捲動而非整份文件;`PageContent height="fill"` 讓頁首、分頁籤、工具列固定,最後一個子元素(`DataGrid`,或包著它的 flex 欄)填滿剩餘高度,表格在固定表頭下捲動列、頁尾 pager 永遠可見。兩者預設 `height="auto"`(不變);SDK `AppShell` 的頁面 `fill: true` 效果相同。`DataGrid` 欄位新增 `priority`:最小寬度放不下時,從最低優先的欄開始隱藏(同優先從最右邊),800px DSM 視窗會少顯示次要欄而不橫向捲動;沒設 `priority` 或 `hideable: false` 的欄一定顯示。因寬度隱藏的欄不會存成使用者隱藏、仍參與快速篩選、視窗變寬即回來,欄位選單中以未勾選加「空間不足」(新 label `hiddenByWidth`,九語系)列出,勾選即在此檢視中顯示。新增 `formatDateTime`:`style: 'compact'` 在所有內建語系都放得進 140px 欄,今年為 `10/17 14:32`、`Oct 17, 2:32 PM`,其他年份只顯示含年份的日期(`2025/3/2`、`Mar 2, 2025`);預設 `full` 含秒,供 tooltip。**可見變更:**執行中的 `CellStatus` 符合一列高度(標籤 16px+間距 2px+新的 `Progress size="sm"` 4px 細條=22px),有進度條的列不再較高;`detail` 仍會多一行。DataGrid 每個儲存格(含 slot)都有 `data-column`。勾選型選單項目新增 `hint`;欄位選單勾選時保持開啟(已寫入文件,行為不變)。Download Hub 升級時:下載頁設 `fill: true`、清單外層改為 flex 欄、選用欄設 `priority`(開始時間 1、速度 2、存放位置 3、大小 4)、時間改用 `formatDateTime` compact 並以完整格式作 `title`、移除只為測試定位而加的 `data-cell`。
14
+
3
15
  ## 0.11.0 (2026-10-17)
4
16
 
5
17
  - `DataGrid` handles long lists by itself. Every column gets a pixel width: a column with `width` keeps it, a column without one is flexible and shares the rest by the new `flex` weight (default 1), never below its `minWidth` (default 80 px for a flexible column). A long file name can no longer take the whole width, a header never wraps one character per line, and the grid scrolls sideways only when the minimums do not fit (a phone) or the user widened columns. A plain cell is cut to one line with its full text in a tooltip; `wrap: true` wraps a column instead. Headers carry their label as a tooltip.
package/README.md CHANGED
@@ -335,6 +335,31 @@ import { AppLayout, Button, PageHeader } from '@synopackageland/ui'
335
335
  `PageHeader` the space is the header's own bottom padding (12px in a DSM
336
336
  window), with no section gap added.
337
337
 
338
+ **Fill-height pages.** A list page whose grid pages its rows should not
339
+ scroll as a whole, or a 50-row page pushes its pager far below. Give both the
340
+ layout and the page `height="fill"`: `AppLayout` then takes its container's
341
+ height (a DSM window) and scrolls its content area instead of the document;
342
+ `PageContent` keeps its header, tabs and toolbar in place, and its **last
343
+ child** takes the height left. Make that child the `DataGrid` itself, or a
344
+ flex column (`flex flex-col min-h-0`) holding it: the grid scrolls its rows
345
+ under its sticky header, with the pager always in view. Pages that are not
346
+ `fill` in the same layout scroll as before. In the SDK's `AppShell`, the same
347
+ is a page's `fill: true`.
348
+
349
+ ```vue
350
+ <AppLayout height="fill" min-height="0">
351
+ <template #sidebar>…</template>
352
+ <PageContent height="fill">
353
+ <PageHeader title="下載" />
354
+ <div class="flex items-center justify-between gap-2">
355
+ <SectionTabs v-model="tab" :items="tabs" />
356
+ <SearchField v-model="filter" variant="filter" />
357
+ </div>
358
+ <DataGrid :columns="columns" :rows="rows" :filter="filter" :page-sizes="[50, 100, 200]" column-chooser />
359
+ </PageContent>
360
+ </AppLayout>
361
+ ```
362
+
338
363
  To set a block apart inside a card, use `<Surface variant="inset">`. DSM draws it as a
339
364
  faint grey block with no border of its own, at 12px × 16px padding.
340
365
 
@@ -502,9 +527,11 @@ phone; use `DataGrid` for anything a user searches or arranges.
502
527
  ```
503
528
 
504
529
  - **Column widths.** A column with `width` (px, or a percentage of the grid) keeps it; a column without one is flexible and shares the rest by `flex` (default 1), never below its `minWidth` (default 80px for a flexible column, 40px for a fixed one). A long text cannot widen its column, and a header never wraps. When the minimums do not fit (a phone), the grid scrolls sideways instead of squeezing.
505
- - **Long text.** A plain cell is cut to one line with an ellipsis and shows its full text on hover (`title`); `wrap: true` wraps a column instead. A `#cell-<key>` slot renders its own content: `CellText` cuts and titles it the same way.
530
+ - **Long text.** A plain cell is cut to one line with an ellipsis and shows its full text on hover (`title`); `wrap: true` wraps a column instead. A `#cell-<key>` slot renders its own content: `CellText` cuts and titles it the same way. Every body cell, a slot's included, carries `data-column="<key>"`.
531
+ - **Row height.** Rows are one height (28px, 24px compact). `CellStatus` with a running `progress` fits it (a 4px bar under the label); its `detail` adds a line, so put speed or a reason in its own column. Dates: `formatDateTime(value, { locale, style: 'compact' })` in the cell (this year: `10/17 14:32`, `Oct 17, 2:32 PM`; another year: the date with its year, `2025/3/2`, `Mar 2, 2025`) and the full `formatDateTime(value, { locale })` as its `title`; it fits a 140px column in every built-in locale.
506
532
  - **Sort.** Click a sortable header, or focus it and press Enter/Space (`aria-sort` is set). Cycle: ascending, descending, off. Leave `sort` unbound and the grid sorts itself (start with `default-sort`); bind `v-model:sort` to control it; with `:local-sort="false"` it only emits.
507
- - **Column chooser.** `column-chooser` adds a columns button at the right of the header: the user shows and hides columns and resets them. `hideable: false` keeps a column, `defaultHidden: true` starts one hidden; the last shown column cannot be hidden. Bind `v-model:hidden-columns` to control it.
533
+ - **Column chooser.** `column-chooser` adds a columns button at the right of the header: the user shows and hides columns and resets them. `hideable: false` keeps a column, `defaultHidden: true` starts one hidden; the last shown column cannot be hidden. Bind `v-model:hidden-columns` to control it. A click on a column toggles it and **keeps the menu open**, so several columns can be changed in a row; Reset, Escape or a click outside closes it.
534
+ - **Column priority (narrow windows).** Give optional columns a `priority`: when the shown columns do not fit at their minimum widths (a fixed `width`, else `minWidth`), the grid leaves out the lowest priority first (the rightmost of a tie first) until the rest fits, so an 800px DSM window loses its least needed columns instead of scrolling sideways. A column without a `priority`, or with `hideable: false`, is never left out, so a grid of such columns still scrolls (a phone). A column left out for width is not hidden by the user: it is not saved, the quick filter still searches it, it returns when the window widens, and the chooser lists it unchecked with "Too narrow" (`labels.hiddenByWidth`). Checking it there shows it anyway for this view; the next lowest priority then makes room, or the grid scrolls.
508
535
  - **Column reorder.** `column-reorder` lets the user drag a header onto another, or focus it and press Alt+Left/Right. Bind `v-model:column-order` to control it; keys of new columns not in a saved order follow at the end.
509
536
  - **Column resize.** Drag the right edge of a header, or focus the edge and press the left/right arrows (10px). Persist widths with `v-model:column-widths` (or `@column-resize`); pass them back to restore. `resizable` is on by default; turn it off per grid or per column.
510
537
  - **Quick filter.** `filter` shows the rows whose shown, searchable columns contain the text (case-insensitive), across every page, before sorting and paging. `searchable: false` leaves a column out, `searchText(row)` gives the text to search, `filter-row(row, text)` replaces the match. Rows that come filtered from a server: `:local-filter="false"`. The footer reads "3 of 240 items" and an empty result says nothing matches.
@@ -512,7 +539,7 @@ phone; use `DataGrid` for anything a user searches or arranges.
512
539
  - **Saved view.** `state-key="downloads"` saves the user's column order, hidden columns, widths, sort and page size in this browser (localStorage) and restores them; bound props win. To keep them on a server, bind the v-models instead, or use the exported `loadGridState` / `saveGridState`. `resetColumns()` (exposed) and the chooser's Reset restore order, visibility and widths.
513
540
  - **Layout.** Sticky header (default), column dividers, hover and selected row colours, `max-height` to scroll inside the grid. `selectable` adds checkboxes; with `select-on-row-click` a row click selects the row and Ctrl/Cmd-click toggles it.
514
541
  - **Footer bar.** `footer` shows the DSM bar with the item count; `refreshable` adds the refresh button (`@refresh`, `refreshing` spins it). Without `footer` it appears when there is more than one page, a page-size choice that matters, or a `#footer` slot.
515
- - **Localisation.** Every built-in string goes through `labels`: `empty`, `noMatch`, `items(count)`, `filtered(count, total)`, `page(page, count)`, `previous`, `next`, `pageSize`, `refresh`, `selectAll`, `selectRow(key)`, `resizeColumn(label)`, `columns`, `resetColumns`. `emptyText` still works.
542
+ - **Localisation.** Every built-in string goes through `labels`: `empty`, `noMatch`, `items(count)`, `filtered(count, total)`, `page(page, count)`, `previous`, `next`, `pageSize`, `refresh`, `selectAll`, `selectRow(key)`, `resizeColumn(label)`, `columns`, `resetColumns`, `hiddenByWidth`. `emptyText` still works.
516
543
  - **Loading.** `loading` masks the table with a centred indicator; `loading-text` adds a message.
517
544
  - **Footer layout.** The footer has top and bottom divider lines and a separator
518
545
  before Refresh. In a flex column with a bounded height, the grid fills the
@@ -1,6 +1,13 @@
1
1
  type __VLS_Props = {
2
2
  stateKey?: string;
3
+ locale?: 'zh-TW' | 'en';
4
+ filter?: string;
5
+ toolbar?: boolean;
3
6
  };
4
- declare const __VLS_export: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
7
+ declare const __VLS_export: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
8
+ filter: string;
9
+ locale: "zh-TW" | "en";
10
+ toolbar: boolean;
11
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
5
12
  declare const _default: typeof __VLS_export;
6
13
  export default _default;
@@ -0,0 +1,8 @@
1
+ type __VLS_Props = {
2
+ locale?: 'zh-TW' | 'en';
3
+ };
4
+ declare const __VLS_export: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
5
+ locale: "zh-TW" | "en";
6
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
7
+ declare const _default: typeof __VLS_export;
8
+ export default _default;
@@ -12,6 +12,12 @@ type __VLS_Props = {
12
12
  brandName?: string;
13
13
  /** Logo URL beside the brand name; defaults to the page icon. */
14
14
  brandIcon?: string;
15
+ /**
16
+ * `'fill'` takes the container's height (a DSM window) and scrolls the
17
+ * content area instead of the document, so a `PageContent height="fill"`
18
+ * page can keep its header and pager in view.
19
+ */
20
+ height?: 'auto' | 'fill';
15
21
  };
16
22
  type __VLS_Slots = {
17
23
  sidebar?: () => unknown;
@@ -22,6 +28,7 @@ type __VLS_Slots = {
22
28
  footer?: () => unknown;
23
29
  };
24
30
  declare const __VLS_base: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
31
+ height: "auto" | "fill";
25
32
  minHeight: string;
26
33
  sidebarWidth: string;
27
34
  responsive: boolean;
@@ -6,12 +6,19 @@ type __VLS_Props = {
6
6
  width?: 'fill' | 'readable' | (string & {});
7
7
  /** Web/portal mode uses a page surface by default. Set flat for card-based dashboards. */
8
8
  surface?: 'auto' | 'flat' | 'card';
9
+ /**
10
+ * `'fill'` takes the container's height and does not scroll: the header,
11
+ * tabs and toolbar stay, and the last child (a DataGrid, or a flex column
12
+ * holding one) takes the height left and scrolls itself.
13
+ */
14
+ height?: 'auto' | 'fill';
9
15
  };
10
16
  declare var __VLS_1: {};
11
17
  type __VLS_Slots = {} & {
12
18
  default?: (props: typeof __VLS_1) => any;
13
19
  };
14
20
  declare const __VLS_base: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
21
+ height: "auto" | "fill";
15
22
  width: "fill" | "readable" | (string & {});
16
23
  surface: "auto" | "flat" | "card";
17
24
  }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
@@ -18,6 +18,12 @@ export type DataGridColumn = {
18
18
  hideable?: boolean;
19
19
  /** Starts hidden; the user can show it from the column chooser. */
20
20
  defaultHidden?: boolean;
21
+ /**
22
+ * Leaves the column out when the grid is too narrow for every column's
23
+ * minimum width: the lowest priority goes first. Without one the column
24
+ * always shows. The column chooser still lists it, to show it anyway.
25
+ */
26
+ priority?: number;
21
27
  /** `false` leaves the column out of the quick filter. */
22
28
  searchable?: boolean;
23
29
  /** The text the quick filter searches for a row; defaults to the cell value. */
@@ -18,3 +18,17 @@ export type ColumnWidthSpec = {
18
18
  * (`0`) gives the minimums, so the grid scrolls rather than squeezes.
19
19
  */
20
20
  export declare function distributeColumnWidths(columns: ColumnWidthSpec[], available: number): Record<string, number>;
21
+ export type ColumnPrioritySpec = {
22
+ key: string;
23
+ /** The least width the column takes: its fixed width or its minimum. */
24
+ minWidth: number;
25
+ /** Lower hides first; without one the column is never hidden for width. */
26
+ priority?: number;
27
+ };
28
+ /**
29
+ * Keys of the columns to leave out so the rest fits `available` at their
30
+ * minimum widths: the lowest priority first, the rightmost of a tie first.
31
+ * Columns without a priority stay, so the grid may still scroll. Unknown
32
+ * space (`0`) hides none.
33
+ */
34
+ export declare function columnsHiddenByWidth(columns: ColumnPrioritySpec[], available: number): string[];
@@ -25,6 +25,8 @@ export type DataGridLabels = {
25
25
  noMatch?: string;
26
26
  /** The page-size chooser. */
27
27
  pageSize?: string;
28
+ /** Beside a column the chooser lists that the width leaves out. */
29
+ hiddenByWidth?: string;
28
30
  };
29
31
  /** `DataGridLabels` after the defaults were merged in: every label is present. */
30
32
  export type ResolvedDataGridLabels = DataGridLabels & {
@@ -36,6 +38,7 @@ export type ResolvedDataGridLabels = DataGridLabels & {
36
38
  filtered: (count: number, total: number) => string;
37
39
  noMatch: string;
38
40
  pageSize: string;
41
+ hiddenByWidth: string;
39
42
  };
40
43
  export declare const defaultDataGridLabels: ResolvedDataGridLabels;
41
44
  /** Locale-aware chrome; the exported English defaults remain available to callers. */
@@ -19,6 +19,8 @@ export type DropdownMenuCheckboxEntry = {
19
19
  disabled?: boolean;
20
20
  danger?: boolean;
21
21
  shortcut?: string;
22
+ /** A quiet note after the label, such as why the item is unchecked. */
23
+ hint?: string;
22
24
  };
23
25
  export type DropdownMenuRadioEntry = {
24
26
  type: 'radio';
@@ -6,8 +6,11 @@ type __VLS_Props = {
6
6
  valueLabel?: string;
7
7
  /** Colours the bar by meaning, such as `warning` for a volume that is nearly full. */
8
8
  tone?: 'primary' | 'success' | 'warning' | 'danger';
9
+ /** `sm` is a 4px line that fits under a label in a dense table row. */
10
+ size?: 'md' | 'sm';
9
11
  };
10
12
  declare const __VLS_export: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
13
+ size: "md" | "sm";
11
14
  value: number;
12
15
  max: number;
13
16
  tone: "primary" | "success" | "warning" | "danger";
@@ -124,6 +124,7 @@ export declare const englishUiText: {
124
124
  readonly "Reorder row {key}": "Reorder row {key}";
125
125
  readonly Columns: "Columns";
126
126
  readonly "Reset columns": "Reset columns";
127
+ readonly "Too narrow": "Too narrow";
127
128
  readonly "{count} of {total} items": "{count} of {total} items";
128
129
  readonly "No matching items": "No matching items";
129
130
  readonly "Sort by {name}": "Sort by {name}";
package/dist/index.d.ts CHANGED
@@ -105,8 +105,8 @@ export { default as RepeatRow } from './components/composite/repeat-rows/RepeatR
105
105
  export { default as FieldCell } from './components/composite/repeat-rows/FieldCell.vue';
106
106
  export { formatFileSize, formatSpeed } from './lib/fileSize';
107
107
  export type { UnknownOption } from './lib/fileSize';
108
- export { formatDuration, formatPercent, formatRelativeTime } from './lib/format';
109
- export type { FormatOptions } from './lib/format';
108
+ export { formatDateTime, formatDuration, formatPercent, formatRelativeTime } from './lib/format';
109
+ export type { DateTimeFormatOptions, FormatOptions } from './lib/format';
110
110
  export { resolveFieldWidth } from './lib/fieldWidth';
111
111
  export type { FieldWidth } from './lib/fieldWidth';
112
112
  export { default as SettingsSection } from './components/composite/settings-section/SettingsSection.vue';