@ti-engine/web-framework 1.20.0 → 1.21.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,49 @@
2
2
 
3
3
  This document will contain the list of changes made to the framework. The format is based on the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification.
4
4
 
5
+ ## Version 1.21.0
6
+
7
+ Two read-only screens the framework now provides for every consumer: **Profile** — which has been a registered
8
+ fragment rendering a two-line placeholder since the shell was written — and a new **About**, so a running instance
9
+ can finally answer "which build is this?" without shell access. Both are the same kind of screen (facts, grouped
10
+ into labelled sections), so the framework owns the screen — fragment, Alpine component, CSS — and an application
11
+ supplies only the content, through two virtual descriptor methods. Design record:
12
+ `docs/superpowers/specs/2026-08-13-profile-and-about-screens-design.md` (CA-99).
13
+
14
+ * feat(web-application): add `getProfileInfo( session )` and `getApplicationInfo( session )` — virtual methods
15
+ returning a display-ready `{ identity, sections }` descriptor, dispatched from `processDataRequest` for the
16
+ `profile` and `about` views. A subclass overrides one or both and inherits the entire screen. Every string in a
17
+ descriptor is resolved server-side, where the session language and the label catalogue are
18
+ * feat(application-info): new `#application-info` module — the pure `buildApplicationInfo()` normalizes a
19
+ `package.json`-shaped manifest (display name derived from the package name, author contact stripped, a `git+…​.git`
20
+ repository URL reduced to a browser-openable homepage) and applies the `TI_WEB_APP_NAME` / `TI_WEB_APP_VERSION` /
21
+ `TI_WEB_APP_RELEASE_DATE` overrides; `readApplicationManifest()` is the one impure half and returns `{}` rather
22
+ than throwing, so a missing or malformed manifest cannot take a request down
23
+ * feat(web-application): register the `about` fragment, and add `buildComponentsConfig( session )` supplying a
24
+ default sidebar user menu (Profile · About · sign-out) so a consuming application gets a working user menu without
25
+ configuring one
26
+ * feat(static): new `frame-about.html` and a rebuilt `frame-profile.html` — content-free renderers over the
27
+ descriptor — plus the `tiScreenProfile` / `tiScreenAbout` Alpine components. The About screen's release, component
28
+ and runtime sections are assembled client-side, where `getLabel` takes a fallback, which is what keeps them
29
+ readable inside an application that loads only its own label catalogue
30
+ * feat(static): add the reusable `.ti-identity-*` / `.ti-info-*` CSS primitives (identity header with avatar or app
31
+ mark, and the two-up grid of label/value sections), plus `.ti-kv-value.mono` / `.mono`-and-`.muted` modifiers
32
+ * feat(localization): add `interface.profile.*`, `interface.about.*`, `interface.topbar.profile|about` and
33
+ `interface.user-menu.*` defaults in en/bg
34
+ * fix(web-application): a framework-owned screen that resolves its own strings server-side would render the
35
+ not-found placeholder inside a consuming application, because an application configures exactly one labels path —
36
+ its own — and never loads `web-server-labels.json`. Every such lookup now passes a readable English literal as the
37
+ `getLabel` fallback added in `@ti-engine/core` 1.10.0 (**requires it**), rather than comparing the result against
38
+ a hard-coded copy of core's placeholder string
39
+ * build(release): bump package version from `1.20.1` to `1.21.0`
40
+
41
+ ## Version 1.20.1
42
+
43
+ * fix(types): emit `/// <reference types="node" />` into `web-server.d.ts`, which names `node:http`.
44
+ Without it a consumer who has not separately configured Node's types gets `TS2591` from inside the
45
+ published declarations. See `@ti-engine/core` 1.9.1 for why the 1.20.0 gate did not catch this
46
+ * build(release): bump package version from `1.20.0` to `1.20.1`
47
+
5
48
  ## Version 1.20.0
6
49
 
7
50
  TypeScript declarations now ship with the package, verified against a consumer type-checking with `skipLibCheck: false`
@@ -23,6 +23,166 @@
23
23
  "bg": "Потребител"
24
24
  }
25
25
  }
26
+ },
27
+ "topbar": {
28
+ "profile": {
29
+ "en": "My Profile",
30
+ "bg": "Моят профил"
31
+ },
32
+ "about": {
33
+ "en": "About",
34
+ "bg": "Информация"
35
+ }
36
+ },
37
+ "user-menu": {
38
+ "profile": {
39
+ "en": "Your profile",
40
+ "bg": "Вашият профил"
41
+ },
42
+ "about": {
43
+ "en": "About",
44
+ "bg": "Информация"
45
+ },
46
+ "logout": {
47
+ "en": "Logout",
48
+ "bg": "Изход"
49
+ }
50
+ },
51
+ "profile": {
52
+ "eyebrow": {
53
+ "en": "Account",
54
+ "bg": "Профил"
55
+ },
56
+ "title": {
57
+ "en": "My Profile",
58
+ "bg": "Моят профил"
59
+ },
60
+ "intro": {
61
+ "en": "Your information as it is recorded in the system. This screen is read-only — to have something corrected, contact your manager.",
62
+ "bg": "Вашата информация, както е записана в системата. Екранът е само за преглед — за корекция се обърнете към вашия мениджър."
63
+ },
64
+ "empty-title": {
65
+ "en": "Profile unavailable",
66
+ "bg": "Профилът е недостъпен"
67
+ },
68
+ "empty-desc": {
69
+ "en": "Your profile information could not be loaded. Try again in a moment.",
70
+ "bg": "Информацията за профила не можа да бъде заредена. Опитайте отново след малко."
71
+ },
72
+ "section-account": {
73
+ "en": "Account",
74
+ "bg": "Профил"
75
+ },
76
+ "section-access": {
77
+ "en": "Access",
78
+ "bg": "Достъп"
79
+ },
80
+ "field-name": {
81
+ "en": "Full name",
82
+ "bg": "Име"
83
+ },
84
+ "field-username": {
85
+ "en": "Username",
86
+ "bg": "Потребител"
87
+ },
88
+ "field-email": {
89
+ "en": "E-mail",
90
+ "bg": "Имейл"
91
+ },
92
+ "field-user-id": {
93
+ "en": "User ID",
94
+ "bg": "Потребителски ID"
95
+ },
96
+ "field-language": {
97
+ "en": "Language",
98
+ "bg": "Език"
99
+ },
100
+ "field-roles": {
101
+ "en": "Roles",
102
+ "bg": "Роли"
103
+ }
104
+ },
105
+ "about": {
106
+ "eyebrow": {
107
+ "en": "Application",
108
+ "bg": "Приложение"
109
+ },
110
+ "title": {
111
+ "en": "About",
112
+ "bg": "Информация"
113
+ },
114
+ "intro": {
115
+ "en": "The application you are running, the release it is built from, and the ti-engine components behind it.",
116
+ "bg": "Приложението, което използвате, версията, от която е изградено, и компонентите на ti-engine зад него."
117
+ },
118
+ "empty-title": {
119
+ "en": "Details unavailable",
120
+ "bg": "Информацията е недостъпна"
121
+ },
122
+ "empty-desc": {
123
+ "en": "The application details could not be loaded. Try again in a moment.",
124
+ "bg": "Информацията за приложението не можа да бъде заредена. Опитайте отново след малко."
125
+ },
126
+ "released": {
127
+ "en": "Released",
128
+ "bg": "Издадена на"
129
+ },
130
+ "section-release": {
131
+ "en": "Release",
132
+ "bg": "Версия"
133
+ },
134
+ "section-components": {
135
+ "en": "Framework components",
136
+ "bg": "Компоненти на платформата"
137
+ },
138
+ "section-components-desc": {
139
+ "en": "The ti-engine packages this application is built on.",
140
+ "bg": "Пакетите на ti-engine, върху които е изградено приложението."
141
+ },
142
+ "section-runtime": {
143
+ "en": "Runtime",
144
+ "bg": "Среда"
145
+ },
146
+ "section-runtime-desc": {
147
+ "en": "Visible to administrators only.",
148
+ "bg": "Видимо само за администратори."
149
+ },
150
+ "field-version": {
151
+ "en": "Version",
152
+ "bg": "Версия"
153
+ },
154
+ "field-release-date": {
155
+ "en": "Release date",
156
+ "bg": "Дата на издаване"
157
+ },
158
+ "field-license": {
159
+ "en": "License",
160
+ "bg": "Лиценз"
161
+ },
162
+ "field-author": {
163
+ "en": "Author",
164
+ "bg": "Автор"
165
+ },
166
+ "field-package": {
167
+ "en": "Package",
168
+ "bg": "Пакет"
169
+ },
170
+ "field-homepage": {
171
+ "en": "Homepage",
172
+ "bg": "Уебсайт"
173
+ },
174
+ "runtime-node": {
175
+ "en": "Node.js",
176
+ "bg": "Node.js"
177
+ },
178
+ "runtime-platform": {
179
+ "en": "Platform",
180
+ "bg": "Платформа"
181
+ },
182
+ "runtime-application": {
183
+ "en": "Application ID",
184
+ "bg": "ID на приложението"
185
+ }
26
186
  }
27
187
  }
28
- }
188
+ }
@@ -29,5 +29,30 @@
29
29
  </span>
30
30
  <span class="ti-sidebar-item-label" x-text="theme === 'daylight' ? 'Glass Theme' : 'Daylight Theme'">Toggle Theme</span>
31
31
  </button>
32
+
33
+ <!--
34
+ User menu. Its entries come from `componentsConfig.userProfileMenu` in the /app/config payload, which
35
+ TiWebAppManager#buildComponentsConfig fills with the framework's own screens (Profile, About) plus
36
+ sign-out — so this menu works in a consuming application that configures nothing.
37
+ -->
38
+ <ti-component-sidebar-flyout-placeholder config-key="userProfileMenu">
39
+ <button class="ti-sidebar-user" type="button"
40
+ x-on:click.stop="toggle()"
41
+ x-on:ti-close-all-flyout="close()"
42
+ x-bind:aria-expanded="isOpen"
43
+ x-bind:aria-controls="$id( 'component-flyout' )"
44
+ x-ref="flyoutButton"
45
+ x-text-label:data-tip="interface.topbar.user-profile"
46
+ data-tip="Your profile">
47
+ <span class="ti-avatar sm"
48
+ x-bind:style="$store.tiToolbox.generateAvatarStyle( $store.tiApplication.user && $store.tiApplication.user.userID, $store.tiApplication.user && $store.tiApplication.user.name )"
49
+ aria-hidden="true"
50
+ x-text="( ( $store.tiApplication.user && $store.tiApplication.user.name ) || '?' ).charAt( 0 ).toUpperCase()">?</span>
51
+ <span class="ti-sidebar-user-text">
52
+ <span class="ti-sidebar-user-name" x-text="( $store.tiApplication.user && $store.tiApplication.user.name ) || ''"></span>
53
+ <span class="ti-sidebar-user-sub" x-text="( $store.tiApplication.user && $store.tiApplication.user.email ) || ''"></span>
54
+ </span>
55
+ </button>
56
+ </ti-component-sidebar-flyout-placeholder>
32
57
  </div>
33
58
  </nav>
@@ -0,0 +1,96 @@
1
+ <!--
2
+ About screen — what this application is and which build of it you are looking at.
3
+
4
+ Like the Profile screen, this is a content-free renderer over a server descriptor: the identity block comes
5
+ from the top-level fields of TiWebAppManager#getApplicationInfo, and everything below it is the normalized
6
+ section list tiScreenAbout assembles (release facts, ti-engine component versions, admin-only runtime facts,
7
+ then whatever sections the application contributed).
8
+
9
+ Alpine runs in CSP mode: no inline style attributes and no optional chaining in expressions.
10
+ -->
11
+ <div class="ti-page" x-data="tiScreenAbout">
12
+
13
+ <div class="ti-page-head">
14
+ <div class="ti-page-eyebrow" x-text-label="interface.about.eyebrow">Application</div>
15
+ <h1 class="ti-page-title" x-text-label="interface.about.title">About</h1>
16
+ <p class="ti-page-subtitle" x-text-label="interface.about.intro">The application you are running, the release
17
+ it is built from, and the ti-engine components behind it.</p>
18
+ </div>
19
+
20
+ <template x-if="!busy && !application">
21
+ <div class="ti-panel">
22
+ <div class="ti-empty-state">
23
+ <div class="ti-empty-state-icon" aria-hidden="true">
24
+ <span class="ti-icon info-circle xl"></span>
25
+ </div>
26
+ <div class="ti-empty-state-title" x-text-label="interface.about.empty-title">Details unavailable</div>
27
+ <div class="ti-empty-state-desc" x-text-label="interface.about.empty-desc">The application details
28
+ could not be loaded. Try again in a moment.</div>
29
+ </div>
30
+ </div>
31
+ </template>
32
+
33
+ <template x-if="application">
34
+ <div>
35
+
36
+ <!-- Identity header — app mark, name, version pill and the one-line description. -->
37
+ <section class="ti-panel">
38
+ <div class="ti-identity">
39
+ <div class="ti-identity-mark" aria-hidden="true">
40
+ <span x-text="applicationInitial()"></span>
41
+ </div>
42
+ <div class="ti-identity-meta">
43
+ <div class="ti-identity-name">
44
+ <span x-text="application.name"></span>
45
+ <span class="ti-tag mono" x-show="application.version"
46
+ x-text="versionTag()"></span>
47
+ </div>
48
+ <div class="ti-identity-caption" x-show="application.description"
49
+ x-text="application.description"></div>
50
+ </div>
51
+ <div class="ti-identity-aside">
52
+ <span class="ti-status-pill muted" x-show="application.releaseDate">
53
+ <span class="ti-icon calendar-blank xs" aria-hidden="true"></span>
54
+ <span x-text="releaseDateText()"></span>
55
+ </span>
56
+ </div>
57
+ </div>
58
+ </section>
59
+
60
+ <!-- Sections — release facts, components, runtime (admin only), then application-contributed ones. -->
61
+ <div class="ti-info-sections">
62
+ <template x-for="(section, sectionIndex) in sections" x-bind:key="sectionIndex">
63
+ <section class="ti-panel ti-info-section" x-bind:class="section.wide ? 'wide' : ''">
64
+ <div class="ti-panel-head bar">
65
+ <div class="ti-panel-head-icon">
66
+ <span class="ti-icon md" x-bind:class="section.icon" aria-hidden="true"></span>
67
+ </div>
68
+ <div class="ti-panel-head-text">
69
+ <div class="ti-panel-title" x-text="section.title"></div>
70
+ <div class="ti-panel-subtitle" x-show="section.description"
71
+ x-text="section.description"></div>
72
+ </div>
73
+ </div>
74
+ <div class="ti-info-grid">
75
+ <template x-for="(item, itemIndex) in section.items" x-bind:key="itemIndex">
76
+ <div class="ti-info-item" x-bind:class="item.wide ? 'wide' : ''">
77
+ <span class="ti-kv-label" x-text="item.label"></span>
78
+ <template x-if="item.href">
79
+ <a class="ti-kv-value ti-info-link" x-bind:href="item.href"
80
+ target="_blank" rel="noopener noreferrer" x-text="item.value"></a>
81
+ </template>
82
+ <template x-if="!item.href">
83
+ <span class="ti-kv-value" x-bind:class="item.valueClass"
84
+ x-text="item.value"></span>
85
+ </template>
86
+ </div>
87
+ </template>
88
+ </div>
89
+ </section>
90
+ </template>
91
+ </div>
92
+
93
+ </div>
94
+ </template>
95
+
96
+ </div>
@@ -1,3 +1,103 @@
1
- <div>
2
- <span>Profile</span>
3
- </div>
1
+ <!--
2
+ Profile screen — a read-only view of the signed-in user's own information.
3
+
4
+ The markup is deliberately content-free: it renders the descriptor returned by
5
+ TiWebAppManager#getProfileInfo (`{ identity, sections }`), which the application overrides to decide WHAT is
6
+ shown while inheriting HOW it is shown. Every string in the descriptor arrives display-ready from the server,
7
+ so nothing here formats or translates data.
8
+
9
+ Alpine runs in CSP mode: no inline style attributes and no optional chaining in expressions — all shaping is
10
+ done in tiScreenProfile before the values reach the template.
11
+ -->
12
+ <div class="ti-page" x-data="tiScreenProfile">
13
+
14
+ <div class="ti-page-head">
15
+ <div class="ti-page-eyebrow" x-text-label="interface.profile.eyebrow">Account</div>
16
+ <h1 class="ti-page-title" x-text-label="interface.profile.title">My Profile</h1>
17
+ <p class="ti-page-subtitle" x-text-label="interface.profile.intro">Your information as it is recorded in the
18
+ system. This screen is read-only — to have something corrected, contact your manager.</p>
19
+ </div>
20
+
21
+ <template x-if="!busy && !profile">
22
+ <div class="ti-panel">
23
+ <div class="ti-empty-state">
24
+ <div class="ti-empty-state-icon" aria-hidden="true">
25
+ <span class="ti-icon user xl"></span>
26
+ </div>
27
+ <div class="ti-empty-state-title" x-text-label="interface.profile.empty-title">Profile unavailable</div>
28
+ <div class="ti-empty-state-desc" x-text-label="interface.profile.empty-desc">Your profile information
29
+ could not be loaded. Try again in a moment.</div>
30
+ </div>
31
+ </div>
32
+ </template>
33
+
34
+ <template x-if="profile">
35
+ <div>
36
+
37
+ <!-- Identity header — avatar, name, meta line and the status pills beside it. -->
38
+ <section class="ti-panel">
39
+ <div class="ti-identity">
40
+ <div class="ti-avatar xl" x-bind:style="avatarStyle()" aria-hidden="true">
41
+ <span x-text="avatarInitial()"></span>
42
+ </div>
43
+ <div class="ti-identity-meta">
44
+ <div class="ti-identity-name">
45
+ <span x-text="profile.identity.name"></span>
46
+ <template x-if="profile.identity.badge">
47
+ <span class="ti-tag" x-bind:class="profile.identity.badge.tone"
48
+ x-text="profile.identity.badge.text"></span>
49
+ </template>
50
+ </div>
51
+ <div class="ti-identity-sub" x-show="profile.identity.subtitle"
52
+ x-text="profile.identity.subtitle"></div>
53
+ <div class="ti-identity-caption" x-show="profile.identity.caption"
54
+ x-text="profile.identity.caption"></div>
55
+ </div>
56
+ <div class="ti-identity-aside">
57
+ <template x-for="(tag, tagIndex) in profile.identity.tags" x-bind:key="tagIndex">
58
+ <span x-bind:class="tag.pillClass">
59
+ <span class="dot" x-show="tag.dot"></span>
60
+ <span x-text="tag.text"></span>
61
+ </span>
62
+ </template>
63
+ </div>
64
+ </div>
65
+ </section>
66
+
67
+ <!-- Sections — one panel per titled group of label/value pairs. -->
68
+ <div class="ti-info-sections">
69
+ <template x-for="(section, sectionIndex) in profile.sections" x-bind:key="sectionIndex">
70
+ <section class="ti-panel ti-info-section" x-bind:class="section.wide ? 'wide' : ''">
71
+ <div class="ti-panel-head bar">
72
+ <div class="ti-panel-head-icon">
73
+ <span class="ti-icon md" x-bind:class="section.icon" aria-hidden="true"></span>
74
+ </div>
75
+ <div class="ti-panel-head-text">
76
+ <div class="ti-panel-title" x-text="section.title"></div>
77
+ <div class="ti-panel-subtitle" x-show="section.description"
78
+ x-text="section.description"></div>
79
+ </div>
80
+ </div>
81
+ <div class="ti-info-grid">
82
+ <template x-for="(item, itemIndex) in section.items" x-bind:key="itemIndex">
83
+ <div class="ti-info-item" x-bind:class="item.wide ? 'wide' : ''">
84
+ <span class="ti-kv-label" x-text="item.label"></span>
85
+ <template x-if="item.href">
86
+ <a class="ti-kv-value ti-info-link" x-bind:href="item.href"
87
+ target="_blank" rel="noopener noreferrer" x-text="item.value"></a>
88
+ </template>
89
+ <template x-if="!item.href">
90
+ <span class="ti-kv-value" x-bind:class="item.valueClass"
91
+ x-text="item.value"></span>
92
+ </template>
93
+ </div>
94
+ </template>
95
+ </div>
96
+ </section>
97
+ </template>
98
+ </div>
99
+
100
+ </div>
101
+ </template>
102
+
103
+ </div>
@@ -2042,6 +2042,145 @@ body {
2042
2042
  color: var(--fg-primary);
2043
2043
  }
2044
2044
 
2045
+ .ti-kv-value.mono {
2046
+ font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Consolas, monospace;
2047
+ font-size: var(--fs-xs);
2048
+ }
2049
+
2050
+ /* Dimmed value — a hint, a placeholder, or a fact that is deliberately secondary. */
2051
+ .ti-kv-value.muted {
2052
+ color: var(--fg-tertiary);
2053
+ }
2054
+
2055
+ /* ============================================================
2056
+ Identity header + info sections — the read-only "facts"
2057
+ pattern behind the Profile and About screens. An application
2058
+ supplies the content as a descriptor; this supplies the
2059
+ layout, so every consumer's screens look the same.
2060
+ ============================================================ */
2061
+ .ti-identity {
2062
+ display: flex;
2063
+ align-items: center;
2064
+ gap: var(--s-4);
2065
+ padding: var(--s-5);
2066
+ }
2067
+
2068
+ /* Square brand/app mark, for an identity block that stands for an application rather than a person
2069
+ (the circular .ti-avatar is the person case). Mirrors the sidebar brand mark at a larger size. */
2070
+ .ti-identity-mark {
2071
+ width: 60px;
2072
+ height: 60px;
2073
+ display: grid;
2074
+ place-items: center;
2075
+ border-radius: var(--r-lg);
2076
+ background: linear-gradient(135deg, var(--accent), color-mix(in oklab, var(--accent) 70%, #000));
2077
+ color: #FFF;
2078
+ font-weight: 700;
2079
+ font-size: 26px;
2080
+ letter-spacing: -0.02em;
2081
+ text-transform: uppercase;
2082
+ box-shadow: 0 4px 12px color-mix(in oklab, var(--accent) 32%, transparent), inset 0 1px 0 rgba(255, 255, 255, 0.25);
2083
+ flex: 0 0 auto;
2084
+ }
2085
+
2086
+ .ti-identity-meta {
2087
+ flex: 1;
2088
+ min-width: 0;
2089
+ }
2090
+
2091
+ .ti-identity-name {
2092
+ display: flex;
2093
+ align-items: center;
2094
+ flex-wrap: wrap;
2095
+ gap: var(--s-2);
2096
+ font-size: var(--fs-2xl);
2097
+ font-weight: 600;
2098
+ letter-spacing: -0.02em;
2099
+ line-height: 1.1;
2100
+ color: var(--fg-primary);
2101
+ }
2102
+
2103
+ .ti-identity-sub {
2104
+ display: flex;
2105
+ align-items: center;
2106
+ flex-wrap: wrap;
2107
+ gap: 6px;
2108
+ font-size: var(--fs-sm);
2109
+ color: var(--fg-secondary);
2110
+ margin-top: var(--s-1);
2111
+ }
2112
+
2113
+ .ti-identity-sub-sep {
2114
+ color: var(--fg-tertiary);
2115
+ }
2116
+
2117
+ .ti-identity-caption {
2118
+ font-size: var(--fs-sm);
2119
+ color: var(--fg-tertiary);
2120
+ margin-top: 2px;
2121
+ overflow-wrap: anywhere;
2122
+ }
2123
+
2124
+ /* Right-hand stack of status pills / tags. */
2125
+ .ti-identity-aside {
2126
+ display: flex;
2127
+ flex-direction: column;
2128
+ align-items: flex-end;
2129
+ gap: var(--s-1);
2130
+ flex: 0 0 auto;
2131
+ }
2132
+
2133
+ /* Two-up grid of section panels; a section may claim the full row with .wide. */
2134
+ .ti-info-sections {
2135
+ display: grid;
2136
+ grid-template-columns: repeat(2, minmax(0, 1fr));
2137
+ gap: var(--s-5);
2138
+ margin-top: var(--s-5);
2139
+ align-items: start;
2140
+ }
2141
+
2142
+ .ti-info-section.wide {
2143
+ grid-column: 1 / -1;
2144
+ }
2145
+
2146
+ @media (max-width: 900px) {
2147
+ .ti-info-sections {
2148
+ grid-template-columns: 1fr;
2149
+ }
2150
+ }
2151
+
2152
+ /* Label/value pairs inside a section, stacked (label above value) rather than the side-by-side
2153
+ rhythm of .ti-kv-grid — a read-only mirror of the .ti-form-grid a matching edit screen uses. */
2154
+ .ti-info-grid {
2155
+ display: grid;
2156
+ grid-template-columns: repeat(2, minmax(0, 1fr));
2157
+ gap: var(--s-4);
2158
+ padding: var(--s-5);
2159
+ }
2160
+
2161
+ .ti-info-item {
2162
+ display: flex;
2163
+ flex-direction: column;
2164
+ gap: 3px;
2165
+ min-width: 0;
2166
+ }
2167
+
2168
+ .ti-info-item.wide {
2169
+ grid-column: 1 / -1;
2170
+ }
2171
+
2172
+ /* An info value that is a destination rather than a fact. Only http(s)/mailto targets ever reach it — the
2173
+ normalizer in ti-framework.js drops anything else before it can become an href. */
2174
+ .ti-info-link {
2175
+ color: var(--accent);
2176
+ text-decoration: none;
2177
+ overflow-wrap: anywhere;
2178
+ }
2179
+
2180
+ .ti-info-link:hover {
2181
+ text-decoration: underline;
2182
+ }
2183
+
2045
2184
  /* ============================================================
2046
2185
  Tag / pill label
2047
2186
  ============================================================ */