@synopackageland/ui 0.9.5 → 0.9.7

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,49 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.7 (2026-10-14)
4
+
5
+ Fixes from the t-0496 re-verification on DSM. **Behavior change:** a `Space` row packs its items at the start instead of centring them.
6
+ 修正 t-0496 DSM 複驗發現的問題。**行為變更:** `Space` 列的項目改為靠起點排列,不再置中。
7
+
8
+ - A `Space` row packs its items at the start of the line (`justify-content: flex-start`); before, it centred them, and every app had to add `justify-content="start"`. Pass `justify-content="center"` for the old layout.
9
+ - `Space` 列的項目靠行首排列(`justify-content: flex-start`);先前預設置中,每個 App 都得加上 `justify-content="start"`。需要舊版排列時傳入 `justify-content="center"`。
10
+ - A stuck `PageHeader` on a full-page (web) card page is opaque from the top edge of its scroller. When `PageContent` itself scrolled (as the SDK's `AppShell` does), the header stuck inside the page's 28px top padding and the content scrolled visibly above it; the space around the card is now a margin and the first header covers the card's top padding.
11
+ - 全頁(web)卡片頁面中,黏住的 `PageHeader` 從捲動區頂端起就是不透明的。先前 `PageContent` 本身是捲動區時(SDK 的 `AppShell` 即是如此),頁首黏在頁面 28px 上內距之內,捲動內容會從頁首上方透出;現在卡片外的間距改為 margin,第一個頁首涵蓋卡片的上內距。
12
+ - A narrow `ConnectionTopology` (below 600px) breaks node labels between words, never inside one: a node is at least as wide as its longest word and the links give up the room (down to 16px). At 300–360px "Cloudflare" was split into "Cloudflar / e".
13
+ - 窄的 `ConnectionTopology`(600px 以下)節點標籤只在詞與詞之間換行,不在詞中斷開:節點至少與最長的詞同寬,由連線讓出空間(最窄 16px)。先前在 300–360px 時「Cloudflare」被斷成「Cloudflar / e」。
14
+ - A `DataGrid` or `DataTable` wider than its box shades the side it continues on (`[data-ui-scroll-edges]`, `data-overflow-start` / `data-overflow-end`; colour `--ui-scroll-edge-shade`). In a 720px full page the last columns were cut off with no sign that the table scrolls, as macOS hides scrollbars until scrolling.
15
+ - 比外框寬的 `DataGrid` 或 `DataTable` 會在還有內容的一側顯示陰影(`[data-ui-scroll-edges]`、`data-overflow-start`/`data-overflow-end`;顏色 `--ui-scroll-edge-shade`)。先前在 720px 全頁時最後幾欄被切掉,且 macOS 在捲動前隱藏捲軸,看不出表格可以捲動。
16
+ - A `CardGrid` (`fit="stretch"`, `minItemWidth` in px or rem) does not leave one card alone on a row: a card may shrink to 90% of `minItemWidth` to join the row, and the columns spread evenly over the rows (3 cards → 3, 4 → 2 + 2, 5 → 3 + 2). Three 360px cards in a 1033px grid were laid out 2 + 1.
17
+ - `CardGrid`(`fit="stretch"`,`minItemWidth` 為 px 或 rem)不會讓一張卡片獨佔一列:卡片可縮到 `minItemWidth` 的 90% 以併入同列,欄數平均分配到各列(3 張 → 3;4 張 → 2 + 2;5 張 → 3 + 2)。先前三張 360px 卡片在 1033px 寬的格線中排成 2 + 1。
18
+
19
+ ## 0.9.6 (2026-10-14)
20
+
21
+ Fixes from the 0.10.3 acceptance run, plus `InputGroup`, `CellText openLabel` and `Space wrap`. **Behavior change:** a floating `Dialog` keeps a 12px margin from the browser viewport and the DSM taskbar, so it can be up to 24px shorter; narrow label columns shrink to 40% of the row.
22
+ 修正 0.10.3 驗收發現的問題,並新增 `InputGroup`、`CellText openLabel` 與 `Space wrap`。**行為變更:** 浮動 `Dialog` 與瀏覽器視窗及 DSM 工作列保持 12px 間距,因此最多矮 24px;窄的標籤欄縮為列寬的 40%。
23
+
24
+ - A sticky `FormActions` only sticks while the scroll area has room for it below the form's top. In a full-page app whose form starts near the bottom of the page, the bar was pinned over the form's first fields and cut off; it now stays in the flow until there is room.
25
+ - 黏性 `FormActions` 只在捲動區於表單頂端下方有足夠空間時才黏在底部。全頁 App 中表單從頁面底部附近開始時,按鈕列原本被固定在表單前幾個欄位上而被截斷;現在空間不足時留在原位。
26
+ - A floating `Dialog` keeps the same 12px margin above (below the DSM taskbar) and below. A dialog as tall as the room touched the taskbar at the top and the viewport edge at the bottom.
27
+ - 浮動 `Dialog` 上方(DSM 工作列下)與下方保持相同的 12px 間距。先前與可用空間等高的對話框,上緣貼著工作列、下緣貼著視窗邊緣。
28
+ - In a narrow card, the label column of `KeyValueRow`, `SettingsRow` and `FormItem` (horizontal) shrinks to at most 40% of the row (`--ui-label-column`), and labels and `KeyValueRow` values break between words, not inside one (`凌晨2:39` stays together).
29
+ - 窄卡片中,`KeyValueRow`、`SettingsRow` 與 `FormItem`(水平)的標籤欄最多佔列寬 40%(`--ui-label-column`),標籤與 `KeyValueRow` 的值在詞與詞之間換行,不在詞中斷開(「凌晨2:39」不拆開)。
30
+ - `Card` footers line up at the bottom of a `CardGrid` row: the body grows to fill a card made taller by its neighbours.
31
+ - `CardGrid` 同一列的 `Card` 底部列對齊在底端:卡片被鄰居撐高時,內容區會填滿多出的高度。
32
+ - `FormItem`s nested in a `SettingsSection` (or any wrapper) inside a `Form` keep the form's field gap; before, only the form's direct children got it and nested fields touched. The automatic gap follows `--ui-field-gap`: 6px in a DSM window (8px vertical), 10px on a full page.
33
+ - `Form` 內巢狀在 `SettingsSection`(或其他包裝)中的 `FormItem` 也套用表單欄位間距;先前只有表單的直接子元素有間距,巢狀欄位彼此相貼。自動間距改用 `--ui-field-gap`:DSM 視窗 6px(垂直 8px),全頁 10px。
34
+ - A two-line `CellText` has a 2px gap between its lines, and `DataTable` cells have 6px vertical padding, so stacked cells no longer touch the row lines.
35
+ - 兩行的 `CellText` 行間有 2px 間距,`DataTable` 儲存格上下各 6px 內距,堆疊的儲存格不再貼著列線。
36
+ - A line break in a `Dialog` or `MessageBox` description stays a line break.
37
+ - `Dialog` 與 `MessageBox` 描述中的換行會保留。
38
+ - `Space` has `wrap`: a row of tags or buttons flows onto more lines with the same gap.
39
+ - `Space` 新增 `wrap`:一排標籤或按鈕空間不足時以相同間距換行。
40
+ - `CellText` has `openLabel`: the main text becomes a link-styled button that emits `open` (for example to open a file in File Station), keeps the ellipsis and shows the label as its tooltip; `openDisabled` disables it.
41
+ - `CellText` 新增 `openLabel`:主文字變成連結樣式的按鈕並發出 `open`(例如在 File Station 開啟檔案),保留省略號並以該標籤作為提示;`openDisabled` 可停用。
42
+ - New `InputGroup` puts a field and the buttons acting on it on one line with an 8px gap; the field takes the room left.
43
+ - 新增 `InputGroup`:欄位與作用於它的按鈕排在同一行,間距 8px,欄位佔剩餘空間。
44
+ - The JSON catalog and `syno-pkg-land-ui describe` cover `CellText`, `CellStatus`, `ListItem` and `InputGroup`.
45
+ - JSON catalog 與 `syno-pkg-land-ui describe` 新增 `CellText`、`CellStatus`、`ListItem` 與 `InputGroup`。
46
+
3
47
  ## 0.9.5 (2026-10-14)
4
48
 
5
49
  Settings rows, buttons, callouts and status colours are restyled; dialogs, menus and lists stay inside the DSM window and below its taskbar. **Behavior change:** a `Button` or `IconButton` whose click handler returns a promise is busy and disabled until it settles and drops a second click. The rest is additive.
package/README.md CHANGED
@@ -505,13 +505,37 @@ Use the pnpm version specified in `package.json`.
505
505
  ```bash
506
506
  pnpm story # component workbench
507
507
  pnpm dev # rebuild dist when library source changes
508
- pnpm check # lint, typecheck, tests, and production build
508
+ pnpm check # production build, then lint, typecheck and tests in parallel, then the package audit
509
+ pnpm check:fast # as check, but only the tests the changes since origin/master reach (vitest --changed)
509
510
  pnpm story:build # build the component workbench
510
511
  pnpm check:host # after pnpm build: DSM host simulation screenshots (Chrome)
511
512
  pnpm check:switch # after pnpm build: Switch knob geometry and click toggling (Chrome)
512
513
  pnpm check:topology # after pnpm build: ConnectionTopology motion, keyboard and narrow layout (Chrome)
513
514
  ```
514
515
 
516
+ Use `pnpm check:fast` while developing (`SYNOPKGLAND_GATE_BASE` sets the base),
517
+ and `pnpm check` once before a release: the full run is the release gate.
518
+ `check` prints one line per passing task and a failing task's whole log.
519
+
520
+ With `SYNOPKGLAND_DOCKER_SSH=<ssh host>`, `pnpm check`, `check:fast` and
521
+ `check:polish` run on that host in Docker instead,
522
+ so a busy machine cannot time out their tests; this machine only sends the
523
+ working tree (untracked files included, `node_modules` and `dist` left out)
524
+ and waits. `scripts/docker-check.sh` runs them there in one of the host's two
525
+ gate slots, in the image `synopkgland-gate-frontend:<tag>` that
526
+ `scripts/gate.Dockerfile` describes (Node.js 24.20.0, pnpm 10.33.2, Debian's
527
+ Chromium and the Noto CJK fonts), after `pnpm install` from the shared
528
+ `synopkgland-cache-npm` volume. `check:polish` builds first and brings its
529
+ screenshots back to `.polish-shots`, even when it fails. Set
530
+ `SYNOPKGLAND_DOCKER_NPM_REGISTRY` when the host cannot reach
531
+ registry.npmjs.org (the lockfile's integrity still holds). Without
532
+ `SYNOPKGLAND_DOCKER_SSH` every check runs here, as before.
533
+ `check:local-browser` stays here with ego-browser: run there, with
534
+ Playwright's Chromium (`LOCAL_BROWSER=playwright`), as
535
+ `bash scripts/docker-check.sh --build --out .dsm-host-shots/local 'node scripts/app-local-browser.mjs'`,
536
+ its two default-modal checks "programmatic focus in A survives with B open"
537
+ fail, because Playwright keeps its page focused and ego-browser does not.
538
+
515
539
  `pnpm check:host` serves `tests/dsm-host` and screenshots each overlay scene in
516
540
  English and Traditional Chinese with headless Chrome (set `CHROME` to its path
517
541
  if it is not in `/Applications`), with and without `host-simulation.css`, a
@@ -632,6 +656,9 @@ Release;既有版號只在 dry-run 放行。npm 11 在演練也會拒絕重複
632
656
  手動 `dry_run: false` 會拒絕;正式發佈只能由推 tag 觸發。
633
657
  Linux motion 使用 `MOTION_BROWSER=playwright`,本機預設 ego-browser 保留。
634
658
  `check:local-browser` 需使用者的互動 ego-browser space,保留為本機驗收項。
659
+ 設了 `SYNOPKGLAND_DOCKER_SSH` 時,`pnpm check`、`check:fast`、`check:polish`
660
+ 改在該主機的 Docker(`synopkgland-gate-frontend` image、gate slot)跑,
661
+ 本機只送工作樹並等待;未設時照舊在本機跑。`check:local-browser` 仍在本機用 ego-browser。
635
662
 
636
663
 
637
664
  ## Form controls and compact settings (0.5.7)
@@ -17,7 +17,7 @@ declare var __VLS_2: `cell-${string}`, __VLS_3: {
17
17
  row: Record<string, unknown>;
18
18
  value: unknown;
19
19
  column: DataTableColumn;
20
- }, __VLS_6: `cell-${string}`, __VLS_7: {
20
+ }, __VLS_12: `cell-${string}`, __VLS_13: {
21
21
  row: Record<string, unknown>;
22
22
  value: unknown;
23
23
  column: DataTableColumn;
@@ -25,7 +25,7 @@ declare var __VLS_2: `cell-${string}`, __VLS_3: {
25
25
  type __VLS_Slots = {} & {
26
26
  [K in NonNullable<typeof __VLS_2>]?: (props: typeof __VLS_3) => any;
27
27
  } & {
28
- [K in NonNullable<typeof __VLS_6>]?: (props: typeof __VLS_7) => any;
28
+ [K in NonNullable<typeof __VLS_12>]?: (props: typeof __VLS_13) => any;
29
29
  };
30
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
31
  compact: boolean;
@@ -6,7 +6,7 @@ type __VLS_Props = {
6
6
  rules?: FormRules;
7
7
  /** `'horizontal'` puts the label beside the control, `'vertical'` above it. */
8
8
  direction?: FormDirection;
9
- /** Gap between items; `'auto'` uses `dense` (horizontal) or `regular` (vertical). */
9
+ /** Gap between items, nested ones included; `'auto'` uses `--ui-field-gap` (horizontal) or 2px more (vertical). */
10
10
  spacing?: FormSpacing;
11
11
  /** Maximum width of every control: `'xs'`–`'lg'`, `'full'` or a CSS length. A FormItem can override it. */
12
12
  fieldWidth?: FieldWidth;
@@ -2,7 +2,7 @@ type __VLS_Props = {
2
2
  align?: 'start' | 'end';
3
3
  /** Indents the actions past a horizontal form's label column, so they line up with the fields. */
4
4
  labelWidth?: string;
5
- /** Indents the actions past the shared label column (`--ui-label-width`). */
5
+ /** Indents the actions past the shared label column (`--ui-label-column`). */
6
6
  inset?: boolean;
7
7
  /** Keeps the actions in view at the bottom of a long scrolling form, over a divider. */
8
8
  sticky?: boolean;
@@ -0,0 +1,18 @@
1
+ import { StyleValue } from 'vue';
2
+ type __VLS_Props = {
3
+ viewportClass?: string;
4
+ viewportStyle?: StyleValue;
5
+ };
6
+ declare var __VLS_1: {};
7
+ type __VLS_Slots = {} & {
8
+ default?: (props: typeof __VLS_1) => any;
9
+ };
10
+ declare const __VLS_base: 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>;
11
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
12
+ declare const _default: typeof __VLS_export;
13
+ export default _default;
14
+ type __VLS_WithSlots<T, S> = T & {
15
+ new (): {
16
+ $slots: S;
17
+ };
18
+ };
@@ -6,15 +6,25 @@ type __VLS_Props = {
6
6
  layout?: 'stacked' | 'inline';
7
7
  /** A leading icon, e.g. a file type. */
8
8
  icon?: Component;
9
+ /** Makes the main text a button that emits `open`; the label is its tooltip, e.g. "Open in File Station". */
10
+ openLabel?: string;
11
+ openDisabled?: boolean;
9
12
  };
10
- declare var __VLS_6: {}, __VLS_8: {};
13
+ declare var __VLS_6: {}, __VLS_8: {}, __VLS_10: {};
11
14
  type __VLS_Slots = {} & {
12
15
  default?: (props: typeof __VLS_6) => any;
13
16
  } & {
14
- secondary?: (props: typeof __VLS_8) => any;
17
+ default?: (props: typeof __VLS_8) => any;
18
+ } & {
19
+ secondary?: (props: typeof __VLS_10) => any;
15
20
  };
16
- declare const __VLS_base: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
21
+ declare const __VLS_base: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
22
+ open: () => any;
23
+ }, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{
24
+ onOpen?: (() => any) | undefined;
25
+ }>, {
17
26
  layout: "stacked" | "inline";
27
+ openDisabled: boolean;
18
28
  }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
19
29
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
20
30
  declare const _default: typeof __VLS_export;
@@ -3,7 +3,8 @@ type __VLS_Props = {
3
3
  minItemWidth?: string;
4
4
  gap?: string;
5
5
  /**
6
- * `'stretch'` (auto-fit) grows the cards to use the whole row; `'fill'`
6
+ * `'stretch'` (auto-fit) grows the cards to use the whole row and, once
7
+ * measured, balances the rows so none holds a lone card; `'fill'`
7
8
  * (auto-fill) keeps empty tracks, so a few cards stay at column width.
8
9
  */
9
10
  fit?: 'stretch' | 'fill';
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Columns for `count` cards in a `width`-wide grid, all in px: as many as fit
3
+ * at the minimum width, or one more if every card keeps 90% of it, then spread
4
+ * evenly over the rows it takes, so no row is left with a lone card
5
+ * (3 → 3, 4 → 2 + 2, 5 → 3 + 2). Unknown without a width or cards.
6
+ */
7
+ export declare function cardGridColumns({ width, count, minItemWidth, gap }: {
8
+ width: number;
9
+ count: number;
10
+ minItemWidth: number;
11
+ gap: number;
12
+ }): number | undefined;
@@ -0,0 +1,13 @@
1
+ declare var __VLS_1: {};
2
+ type __VLS_Slots = {} & {
3
+ default?: (props: typeof __VLS_1) => any;
4
+ };
5
+ declare const __VLS_base: import('vue').DefineComponent<{}, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
6
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
7
+ declare const _default: typeof __VLS_export;
8
+ export default _default;
9
+ type __VLS_WithSlots<T, S> = T & {
10
+ new (): {
11
+ $slots: S;
12
+ };
13
+ };
@@ -1,5 +1,5 @@
1
1
  import { SpaceAlign, SpaceDirection, SpaceJustify } from './space';
2
- /** Lays children out in one line with a shared gap and alignment. */
2
+ /** Lays children out in one line (or, with `wrap`, several) with a shared gap and alignment. */
3
3
  type __VLS_Props = {
4
4
  alignItems?: SpaceAlign;
5
5
  /** @deprecated Use `alignItems`; kept so existing templates keep working. */
@@ -8,6 +8,8 @@ type __VLS_Props = {
8
8
  direction?: SpaceDirection;
9
9
  /** A spacing name (`regular`) or a pixel number. */
10
10
  spacing?: 'none' | 'dense' | 'compact' | 'regular' | 'relaxed' | number;
11
+ /** Lets the items wrap onto more lines, with the same gap between lines. */
12
+ wrap?: boolean;
11
13
  };
12
14
  declare var __VLS_1: {};
13
15
  type __VLS_Slots = {} & {
@@ -16,6 +18,7 @@ type __VLS_Slots = {} & {
16
18
  declare const __VLS_base: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
17
19
  direction: SpaceDirection;
18
20
  spacing: "none" | "dense" | "compact" | "regular" | "relaxed" | number;
21
+ wrap: boolean;
19
22
  }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
20
23
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
21
24
  declare const _default: typeof __VLS_export;
@@ -7,4 +7,5 @@ export type JustifyContentCss = AlignItemsCss | 'space-between' | 'space-around'
7
7
  export declare function spaceGap(spacing: SpaceSpacing | number): string;
8
8
  /** A row centres its items; a column stretches them, as SVC v11 does. */
9
9
  export declare function spaceAlignItems(direction: SpaceDirection, alignItems: SpaceAlign | undefined): AlignItemsCss;
10
+ /** Items pack at the start of the line unless asked otherwise. */
10
11
  export declare function spaceJustifyContent(justifyContent: SpaceJustify | undefined): JustifyContentCss;
@@ -0,0 +1,8 @@
1
+ import { ShallowRef } from 'vue';
2
+ /** Whether a horizontal scroller has more content before (`start`) or after
3
+ * (`end`) what it shows. Follows scrolling and size changes of the scroller
4
+ * and of its content. */
5
+ export declare function useScrollEdges(viewport: Readonly<ShallowRef<HTMLElement | null>>): {
6
+ start: import('vue').Ref<boolean, boolean>;
7
+ end: import('vue').Ref<boolean, boolean>;
8
+ };
@@ -0,0 +1,8 @@
1
+ import { Ref } from 'vue';
2
+ /**
3
+ * Whether a bottom-sticky element fits between the top of its parent and the
4
+ * bottom of its scrolling area. A sticky box never rises above its parent, so
5
+ * while the parent starts closer to that edge than the box is tall, sticking
6
+ * would hold the box past the edge, cut off; it waits at the parent's end.
7
+ */
8
+ export declare function useStickyRoom(element: Ref<HTMLElement | null>, enabled: () => boolean): Ref<boolean, boolean>;
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ export { default as Button } from './components/primitives/button/Button.vue';
2
2
  export { default as IconButton } from './components/primitives/button/IconButton.vue';
3
3
  export { default as Input } from './components/primitives/input/Input.vue';
4
4
  export { default as Textarea } from './components/primitives/input/Textarea.vue';
5
+ export { default as InputGroup } from './components/primitives/input-group/InputGroup.vue';
5
6
  export { default as InputSteppers } from './components/primitives/input-steppers/InputSteppers.vue';
6
7
  export type { InputSteppersMenuItem } from './components/primitives/input-steppers/InputSteppers.vue';
7
8
  export { default as Slider } from './components/primitives/slider/Slider.vue';