@synopackageland/ui 0.20.10 → 0.20.12

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,26 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 0.20.12 (2026-11-05)
6
+
7
+ - On a short screen (a phone in landscape, at most 480px tall) outside the phone layout, every App gets a short mode: the page title shows in the bar above the page, beside the toolbar's actions, instead of only for screen readers; a settings page (a `SettingsPage` or a `SectionTabs fill`) scrolls as one, so the bar and header scroll away while the section tabs stay at the top and Reset / Apply at the bottom; and a `PageHeader` scrolls with the page in a tight row. A phone in portrait and a taller window are unchanged. New exports `SHORT_VIEWPORT_QUERY` and `APP_SHORT_KEY` (docs/adaptive-layout.md, "Short screens")
8
+ - A `SectionTabs fill` strip, and on a phone the Back header of a pushed section, sticks below a sticky `PageHeader` in the same `PageContent` instead of sliding under it when the page scrolls as a whole
9
+ - FilePicker on Android no longer makes Chrome ask for the camera or microphone: when `accept` names a video or audio type (`video/*`, `.mp4`, `.mp3`), the file chooser gets `application/octet-stream` (every file, no recording), and a chosen file outside `accept` is still rejected. The new `native-accept` prop sets the chooser's type when it should differ from `accept` (README, FilePicker)
10
+ - Grid: a record link draws as a grey chip in the cell's text colour (like a select option without a colour), no longer in the link blue
11
+ - Grid: a click on an attachment thumbnail asks to open it (`open-attachment`) on the first click, as a checkbox or link icon does; record chips still open only on a cell already selected
12
+
13
+ ## 0.20.11 (2026-11-05)
14
+
15
+ - Button centres its icon on the label: an icon (an `svg`, or an element marked `data-icon`, which `Icon` and `StatusDot` now carry) sits on the centre of the label's line instead of DSM's x-height middle, which put a 16px icon 0.6-1.6px under the label depending on the font. The label keeps its ellipsis
16
+ - Button counts only an `svg` or a `data-icon` element as the icon before or after its text. A `<span>` around the text is text, so `<Button><Mic /><span>Record</span></Button>` keeps one 8px gap instead of 16px, and a `v-if` comment counts as nothing. The label is plain text (docs/button-policy.md, rule 10). CopyButton and FacetPanel mark their icon and count with `data-icon`
17
+ - A Callout inside a `SettingsSection` body sits 12px from the rows around it instead of flush on the next row; a closed Callout takes no gap
18
+ - On a phone, a `SectionTabs` section pushed from its list spans the page's content area: its Back header reaches both edges and its content keeps the page gutter, instead of a white page inside the gutter with its fields on its edges. The list keeps its inset
19
+ - A pushed page's Back header never cuts Back short: its sides keep their full width and a long title ends in its ellipsis first (SectionTabs, ListDetail)
20
+ - FilePicker leaves the file count out of its limits line when the limit is 1 ("up to 1 files"), and the new `show-hint` prop (default on) hides the built-in line of types, size and count when the page states them itself
21
+ - An `AppTabBar` set to `:more="false"` is the whole phone navigation: while it shows, the phone bar drops its menu button, which opened a drawer of the same destinations. A hidden bar brings the button back. Leave `more` unset to keep the drawer reachable through More
22
+ - Docs: the dark scheme attribute belongs on the app root (`AppRoot color-scheme`); `<html data-ui-color-scheme="dark">` works only with `theme.css`, never under the embedded stylesheets. WizardStep lays its content out in block flow (docs/wizard.md, Layout)
23
+
3
24
  ## 0.20.10 (2026-11-05)
4
25
 
5
26
  - A page of cards now sits on a lighter near-white canvas (`--ui-page-canvas`) so it matches pages without a background; `--ui-app-web-canvas` still overrides it
package/README.md CHANGED
@@ -476,9 +476,13 @@ readable on it, and leaves the status tones DSM's:
476
476
  </AppRoot>
477
477
  ```
478
478
 
479
- The theme has a dark scheme too: `<AppRoot color-scheme="dark">` (or
480
- `data-ui-color-scheme="dark"` on `<html>` for a full page) switches every
481
- token, and derives the accent for a dark page. The host decides when, for
479
+ The theme has a dark scheme too: `<AppRoot color-scheme="dark">` switches
480
+ every token, and derives the accent for a dark page. AppRoot puts
481
+ `data-ui-color-scheme="dark"` on its root (`[data-ui-app]`); set the attribute
482
+ there yourself only without AppRoot. The embedded stylesheets
483
+ (`themes/dsm7-embedded*.css`, a DSM window) read it only on `[data-ui-app]`, never on
484
+ the host's `<html>`; `<html data-ui-color-scheme="dark">` works only with
485
+ `theme.css`, on a page of your own. The host decides when, for
482
486
  example from `prefers-color-scheme` ([colour system](docs/color-system.md#dark-theme)).
483
487
 
484
488
  Other tokens are overridden in a stylesheet imported after the library's:
@@ -1206,7 +1210,18 @@ partial overrides of `title`, `description`, `chooseFiles`, `chooseFolder`,
1206
1210
  `titleOne`, `descriptionOne`, `chooseFile`, `acceptedTypes`, `images`,
1207
1211
  `videos`, `audio`, `maxSize`, `maxFiles`, `remove`,
1208
1212
  `typeRejected`, `sizeRejected`, `countRejected`, `duplicateRejected`.
1209
- The messages use `{name}`, `{size}`, `{count}` and `{types}` placeholders. Existing
1213
+ The messages use `{name}`, `{size}`, `{count}` and `{types}` placeholders.
1214
+
1215
+ On Android, Chrome asks for the camera when the file chooser's `accept` names
1216
+ a video type and for the microphone when it names an audio type (extensions
1217
+ count: `.mp4` is video, `.mp3` audio), so that it can offer recording. When
1218
+ `accept` names one, FilePicker gives the chooser `application/octet-stream`
1219
+ on Android instead: the plain document chooser, every file, no prompt. The hint
1220
+ still names the `accept` types, and a chosen file outside them is rejected with
1221
+ `type`. Desktop browsers and iOS get `accept` unchanged. `native-accept` sets
1222
+ the chooser's type everywhere when it should differ from `accept` (an empty
1223
+ string shows every file); set it to the `accept` value to keep recording on
1224
+ Android. Existing
1210
1225
  `title`/`description` props still win. Other controls retain their exposed text
1211
1226
  props and partial text objects; built-in text throughout the library now follows
1212
1227
  Provider locale. Explicit strings, including `''`, win over built-in translations.
@@ -3,21 +3,23 @@ interface PageTitleEntry {
3
3
  title: () => string;
4
4
  }
5
5
  /**
6
- * The phone bar's page title. A PageHeader in the layout's content registers
7
- * its title; the last one mounted is the page's, and the bar shows it unless
8
- * AppLayout's own `title` prop is set.
6
+ * The bar's page title (the phone bar, or the bar of a short screen). A
7
+ * PageHeader in the layout's content registers its title; the last one
8
+ * mounted is the page's, and the bar shows it unless AppLayout's own `title`
9
+ * prop is set.
9
10
  */
10
11
  export interface AppPageTitleHost {
11
12
  /** The content column: a PageHeader outside it (in a dialog or sheet) keeps its own title. */
12
13
  main: Readonly<Ref<HTMLElement | null>>;
13
14
  entries: Ref<readonly PageTitleEntry[]>;
14
- /** Whether the bar shows this entry's title (a phone, and no `title` prop). */
15
+ /** Whether the bar shows this entry's title, so its PageHeader keeps the heading for screen readers only. */
15
16
  shows: (entry: PageTitleEntry) => boolean;
16
17
  }
17
18
  export declare const APP_PAGE_TITLE_KEY: InjectionKey<AppPageTitleHost>;
18
- export declare function createPageTitleHost(main: Readonly<Ref<HTMLElement | null>>, active: () => boolean): AppPageTitleHost;
19
+ /** `active` says whether the bar shows a page title of this text (it is the page's last PageHeader). */
20
+ export declare function createPageTitleHost(main: Readonly<Ref<HTMLElement | null>>, active: (title: string) => boolean): AppPageTitleHost;
19
21
  /**
20
- * Hands a PageHeader's title to the phone bar. Returns whether the bar shows
22
+ * Hands a PageHeader's title to the layout's bar. Returns whether the bar shows
21
23
  * it, so the header can keep its own heading for screen readers only.
22
24
  */
23
25
  export declare function usePageTitle(element: Readonly<Ref<HTMLElement | null>>, title: () => string): ComputedRef<boolean>;
@@ -13,7 +13,9 @@ type __VLS_Props = {
13
13
  modelValue?: string;
14
14
  /**
15
15
  * Adds a last More tab. Defaults to on inside an AppLayout with a sidebar,
16
- * where it opens the navigation drawer; it always emits `more`.
16
+ * where it opens the navigation drawer; it always emits `more`. Set to
17
+ * `false` when the tabs are the whole navigation: while the bar shows,
18
+ * the phone bar then drops its menu button too.
17
19
  */
18
20
  more?: boolean;
19
21
  /** Hides the bar, for example while a full-screen drawer covers the page. */
@@ -0,0 +1,5 @@
1
+ import { Ref } from 'vue';
2
+ /** For PageContent: the tallest height its sticky headers report, in px. */
3
+ export declare function providePageSticky(): Readonly<Ref<number>>;
4
+ /** For PageHeader: reports its height while it sticks, and nothing once it scrolls with the page. */
5
+ export declare function useStickyHeight(element: Readonly<Ref<HTMLElement | null>>, sticks: () => boolean): void;
@@ -8,7 +8,18 @@ export type FileRejection = {
8
8
  type __VLS_Props = {
9
9
  modelValue?: File[];
10
10
  multiple?: boolean;
11
+ /** The file types taken (`.mp3,audio/*`), as the input's `accept`: chosen, dropped or pasted files of other types are rejected. */
11
12
  accept?: string;
13
+ /**
14
+ * The `accept` the browser's file chooser gets, when it should differ from
15
+ * `accept`; `accept` still decides which chosen files are taken, and an
16
+ * empty string lets the chooser show every file. Unset, it is `accept`,
17
+ * except on Android when `accept` names a video or audio type (`video/*`,
18
+ * `.mp4`, `.mp3`): Chrome there asks for the camera or microphone to offer
19
+ * recording, so the chooser gets `application/octet-stream` (every file,
20
+ * nothing to record). Set it to `accept` to keep recording on Android.
21
+ */
22
+ nativeAccept?: string;
12
23
  disabled?: boolean;
13
24
  maxFiles?: number;
14
25
  maxSize?: number;
@@ -22,6 +33,11 @@ type __VLS_Props = {
22
33
  text?: Partial<FilePickerText>;
23
34
  title?: string;
24
35
  description?: string;
36
+ /**
37
+ * Shows the built-in line of limits (accepted types, maximum size, file
38
+ * count) under the picker. Turn it off when the page states the limits itself.
39
+ */
40
+ showHint?: boolean;
25
41
  };
26
42
  declare var __VLS_53: {
27
43
  file: File;
@@ -46,6 +62,7 @@ declare const __VLS_base: import('vue').DefineComponent<__VLS_Props, {}, {}, {},
46
62
  maxFiles: number;
47
63
  maxSize: number;
48
64
  directory: boolean;
65
+ showHint: boolean;
49
66
  }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
50
67
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
51
68
  declare const _default: typeof __VLS_export;
@@ -190,8 +190,8 @@ export interface GridCellKindRenderer<V extends GridCellValue = GridCellValue> {
190
190
  /** Plain text for assistive tech and copy. */
191
191
  describe: (value: V, context: GridCellTextContext) => string;
192
192
  /**
193
- * Target under the pointer, if any. Checkbox, rating, link icon and button
194
- * act on the first click; chips and thumbnails only when `wasActive`.
193
+ * Target under the pointer, if any. Checkbox, rating, link icon, button and
194
+ * attachment thumbnail act on the first click; record chips only when `wasActive`.
195
195
  */
196
196
  hitTest?: (value: V, input: GridCellHitInput) => GridCellHit | null;
197
197
  /** Keyboard equivalent of the hit areas (Enter, Space, digits, Backspace). */
package/dist/grid.js CHANGED
@@ -3668,15 +3668,15 @@ const Xe = (e, t) => e.x >= t.x && e.x < t.x + t.width && e.y >= t.y && e.y < t.
3668
3668
  describe: (e) => dt(e.options.map((t) => t.label))
3669
3669
  }, gr = {
3670
3670
  draw: (e, t, n) => {
3671
- const o = { fill: t.cellTheme.badgeFill, color: t.cellTheme.link }, i = xo(e, t, n.records.map((a) => ({ label: a.label, colors: o })));
3671
+ const o = { fill: t.cellTheme.badgeFill, color: t.cellTheme.text }, i = xo(e, t, n.records.map((a) => ({ label: a.label, colors: o })));
3672
3672
  Nt.set(He("link", t, n.records.map((a) => a.id)), i);
3673
3673
  },
3674
3674
  describe: (e) => dt(e.records.map((t) => t.label)),
3675
3675
  hitTest: (e, t) => Ro(Nt.get(He("link", Xt(t), e.records.map((n) => n.id))), t, (n) => ({ type: "open-record", id: e.records[n].id }))
3676
- }, Xt = (e) => ({ width: e.size.width, metrics: e.metrics }), Ro = (e, t, n) => {
3677
- if (!t.wasActive || !e) return null;
3678
- const o = e.findIndex((i) => Xe(t.point, i));
3679
- return o < 0 ? null : { action: n(o), area: e[o] };
3676
+ }, Xt = (e) => ({ width: e.size.width, metrics: e.metrics }), Ro = (e, t, n, o = !1) => {
3677
+ if (!t.wasActive && !o || !e) return null;
3678
+ const i = e.findIndex((a) => Xe(t.point, a));
3679
+ return i < 0 ? null : { action: n(i), area: e[i] };
3680
3680
  }, fr = (e, t, n, o, i, a, s) => {
3681
3681
  const { cellTheme: r, theme: c } = t, l = s ? t.images.get(s) : void 0;
3682
3682
  if (e.save(), e.beginPath(), e.arc(n + i / 2, o + i / 2, i / 2, 0, Math.PI * 2), e.closePath(), l)
@@ -3722,7 +3722,7 @@ const Xe = (e, t) => e.x >= t.x && e.x < t.x + t.width && e.y >= t.y && e.y < t.
3722
3722
  }), l.more > 0 && $t(e, t, h, l.more, l.badgeWidth), Lt.set(He("attachment", t, n.files.map((g) => g.id)), m);
3723
3723
  },
3724
3724
  describe: (e) => dt(e.files.map((t) => t.name)),
3725
- hitTest: (e, t) => Ro(Lt.get(He("attachment", Xt(t), e.files.map((n) => n.id))), t, (n) => ({ type: "open-attachment", id: e.files[n].id }))
3725
+ hitTest: (e, t) => Ro(Lt.get(He("attachment", Xt(t), e.files.map((n) => n.id))), t, (n) => ({ type: "open-attachment", id: e.files[n].id }), !0)
3726
3726
  }, Qn = (e) => `${Math.round(Math.max(0, Math.min(1, e)) * 100)}%`, wr = {
3727
3727
  draw: (e, t, n) => {
3728
3728
  if (n.value == null) return;
@@ -13,6 +13,7 @@ export type FilePickerText = {
13
13
  images: string;
14
14
  videos: string;
15
15
  audio: string;
16
+ /** `maxFiles` shows only for a limit of two or more, so it is always plural. */
16
17
  maxSize: string;
17
18
  maxFiles: string;
18
19
  remove: string;
package/dist/index.d.ts CHANGED
@@ -240,7 +240,7 @@ export { default as AsidePanel } from './components/app/aside-panel/AsidePanel.v
240
240
  export { clampAsideWidth, readAsidePreference, useAsidePreference, writeAsidePreference } from './components/app/aside-panel/asidePreference';
241
241
  export type { AsidePreference } from './components/app/aside-panel/asidePreference';
242
242
  export { default as ResponsiveView } from './components/app/responsive-view/ResponsiveView.vue';
243
- export { APP_ASIDE_KEY, APP_SIDE_MOTION_KEY, appPresentationForWidth, MOBILE_LAYOUT_MAX, WEB_ASIDE_DEFAULT, WEB_ASIDE_MAX, WEB_ASIDE_MIN, WEB_LAYOUT_MIN, WEB_RAIL_BELOW, WEB_RAIL_WIDTH } from './lib/adaptiveLayout';
243
+ export { APP_ASIDE_KEY, APP_SHORT_KEY, APP_SIDE_MOTION_KEY, appPresentationForWidth, MOBILE_LAYOUT_MAX, SHORT_VIEWPORT_QUERY, WEB_ASIDE_DEFAULT, WEB_ASIDE_MAX, WEB_ASIDE_MIN, WEB_LAYOUT_MIN, WEB_RAIL_BELOW, WEB_RAIL_WIDTH } from './lib/adaptiveLayout';
244
244
  export type { AppAside, AppAsideState, AppNavigation, AppPresentation, WideLayout } from './lib/adaptiveLayout';
245
245
  export { default as AppTabBar } from './components/app/app-tab-bar/AppTabBar.vue';
246
246
  export type { AppTabBarItem } from './components/app/app-tab-bar/AppTabBar.vue';