PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.10
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.10
1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 0.8.7 All 34 releases
desktop-mode / apps / station-home / parts / view.php

view.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.1.10, at apps/station-home/parts/view.php

442 lines 16.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Station Home — the body.
4 *
5 * The Editorial Flight Deck, painted on the server as a function of
6 * the snapshot and the one state key (`customizing`): the identity
7 * rail with its quick actions, the greeting, Continue working, Site
8 * pulse, Needs attention, From your plugins and the Customize modal.
9 * Same classes the stylesheet was written against, same nodes the
10 * legacy bundle built by hand — the framework's morph keeps them
11 * across every repaint.
12 *
13 * Every string that came from a post, an option or a plugin goes
14 * through `text()`: WordPress hands them over texturized
15 * (`&#8217;` for a curly apostrophe), so they are decoded once and
16 * escaped once, never printed as a literal entity and never handed
17 * to the parser as markup.
18 *
19 * @package OpenStation
20 */
21
22 namespace OpenStation\Apps\StationHome;
23
24 use OpenStation\App\Os;
25 use OpenStation\App\State;
26 use function OpenStation\App\Html\esc;
27 use function OpenStation\App\Html\tag;
28
29 defined( 'ABSPATH' ) || exit;
30
31 /**
32 * Escape an entity-bearing string for a text node or an attribute.
33 *
34 * @param mixed $value Anything stringable.
35 * @return string
36 */
37 function text( $value ) {
38 return esc( html_entity_decode( (string) $value, ENT_QUOTES | ENT_HTML5, 'UTF-8' ) );
39 }
40
41 /**
42 * A time-aware greeting.
43 *
44 * @param int $hour 0–23.
45 * @param string $name The person's name.
46 * @return string
47 */
48 function greeting( $hour, $name ) {
49 if ( $hour < 12 ) {
50 /* translators: %s: current user's name. */
51 return sprintf( __( 'Good morning, %s', 'desktop-mode' ), $name );
52 }
53 if ( $hour < 18 ) {
54 /* translators: %s: current user's name. */
55 return sprintf( __( 'Good afternoon, %s', 'desktop-mode' ), $name );
56 }
57 /* translators: %s: current user's name. */
58 return sprintf( __( 'Good evening, %s', 'desktop-mode' ), $name );
59 }
60
61 /**
62 * A decorative Dashicon.
63 *
64 * @param string $class Dashicons class.
65 * @return string
66 */
67 function icon( $class ) {
68 return '<span class="dashicons ' . esc( $class ) . '" aria-hidden="true"></span>';
69 }
70
71 /**
72 * A card's icon: a Dashicon, or an image the plugin registered.
73 *
74 * @param string $value Dashicons class or image URL.
75 * @return string
76 */
77 function card_icon( $value ) {
78 if ( 0 === strpos( $value, 'dashicons-' ) ) {
79 return icon( $value );
80 }
81 return tag(
82 'img',
83 array(
84 'src' => $value,
85 'alt' => '',
86 'loading' => 'lazy',
87 )
88 );
89 }
90
91 /**
92 * The badge tone for a post status.
93 *
94 * @param string $status Post status.
95 * @return string
96 */
97 function status_tone( $status ) {
98 switch ( $status ) {
99 case 'publish':
100 return 'success';
101 case 'pending':
102 case 'future':
103 return 'warning';
104 case 'private':
105 return 'info';
106 default:
107 return 'neutral';
108 }
109 }
110
111 /**
112 * One quick action: a link when it navigates, a button when it calls
113 * into the shell (see `quick_actions()`).
114 *
115 * @param array<string,mixed> $action Quick action.
116 * @return string
117 */
118 function quick_action( array $action ) {
119 $inner = icon( $action['icon'] ) . '<span>' . esc( $action['label'] ) . '</span>';
120 if ( in_array( $action['kind'], array( 'url', 'external' ), true ) && ! empty( $action['url'] ) ) {
121 return tag(
122 'a',
123 array(
124 'class' => 'os-station-home__action',
125 'os-key' => $action['id'],
126 'href' => $action['url'],
127 'title' => $action['label'],
128 'target' => 'external' === $action['kind'] ? '_blank' : false,
129 'rel' => 'external' === $action['kind'] ? 'noopener' : false,
130 ),
131 $inner
132 );
133 }
134 // Without `fill-cell` the host stretches to the grid cell but the
135 // shadow button inside stays shrink-to-fit, so these rows would
136 // end at their label while their `<a>` siblings run the width of
137 // the rail — and lose the 48px min-height the rest of the list keeps.
138 return tag(
139 'os-button',
140 array(
141 'class' => 'os-station-home__action',
142 'os-key' => $action['id'],
143 'variant' => 'ghost',
144 'fill-cell' => true,
145 'title' => $action['label'],
146 'os-action' => 'launch',
147 'os-arg-id' => $action['id'],
148 ),
149 $inner
150 );
151 }
152
153 /**
154 * The whole body.
155 *
156 * @param State $state State.
157 * @param Os $os Host handle.
158 * @return void
159 */
160 function render( State $state, Os $os ) {
161 $snapshot = snapshot( $os );
162 $hour = (int) $os->env->format_datetime( time(), 'G' );
163 ?>
164 <div class="desktop-mode-station-home" data-os-station-home-root>
165 <div class="os-station-home__layout">
166 <aside class="os-station-home__rail" aria-label="<?php esc_attr_e( 'Station Home', 'desktop-mode' ); ?>">
167 <div class="os-station-home__brand">
168 <img
169 class="os-station-home__brand-mark"
170 src="<?php echo esc_url( OPENSTATION_URL . 'assets/images/openstation-mark.svg' ); ?>"
171 alt=""
172 width="36"
173 height="36"
174 />
175 <span>OpenStation</span>
176 </div>
177 <div class="os-station-home__location" aria-current="page">
178 <span aria-hidden="true"></span>
179 <?php esc_html_e( 'Station Home', 'desktop-mode' ); ?>
180 </div>
181 <div class="os-station-home__mesh" aria-hidden="true"></div>
182 <nav class="os-station-home__actions" aria-label="<?php esc_attr_e( 'Quick actions', 'desktop-mode' ); ?>">
183 <?php
184 foreach ( $snapshot['quickActions'] as $action ) {
185 echo quick_action( $action ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Built with the framework's escaping helpers.
186 }
187 ?>
188 </nav>
189 </aside>
190
191 <main class="os-station-home__main">
192 <header class="os-station-home__intro">
193 <div>
194 <h1 id="os-station-home-title"><?php echo text( greeting( $hour, $snapshot['userName'] ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- text() escapes. ?></h1>
195 <p>
196 <?php
197 if ( '' !== (string) $snapshot['siteName'] ) {
198 /* translators: %s: site name. */
199 echo text( sprintf( __( 'Pick up where you left off on %s.', 'desktop-mode' ), $snapshot['siteName'] ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- text() escapes.
200 } else {
201 esc_html_e( 'Pick up where you left off.', 'desktop-mode' );
202 }
203 ?>
204 </p>
205 </div>
206 <os-button
207 class="os-station-home__refresh"
208 variant="ghost"
209 os-action="refresh"
210 aria-label="<?php esc_attr_e( 'Refresh Station Home', 'desktop-mode' ); ?>"
211 title="<?php esc_attr_e( 'Refresh', 'desktop-mode' ); ?>"
212 >
213 <span class="dashicons dashicons-update" aria-hidden="true"></span>
214 </os-button>
215 </header>
216
217 <section class="os-station-home__section" aria-labelledby="os-station-home-work-heading">
218 <h2 id="os-station-home-work-heading"><?php esc_html_e( 'Continue working', 'desktop-mode' ); ?></h2>
219 <div class="os-station-home__work">
220 <?php work( $snapshot['work'] ); ?>
221 </div>
222 </section>
223
224 <section class="os-station-home__section" aria-labelledby="os-station-home-pulse-heading">
225 <h2 id="os-station-home-pulse-heading"><?php esc_html_e( 'Site pulse', 'desktop-mode' ); ?></h2>
226 <div class="os-station-home__pulse">
227 <?php foreach ( $snapshot['metrics'] as $metric ) : ?>
228 <article class="os-station-home__metric" os-key="<?php echo esc( $metric['id'] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- esc() escapes. ?>">
229 <div class="os-station-home__metric-label">
230 <?php echo icon( $metric['icon'] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- icon() escapes. ?>
231 <span><?php echo esc( $metric['label'] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- esc() escapes. ?></span>
232 </div>
233 <strong><?php echo esc( number_format_i18n( $metric['value'] ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- esc() escapes. ?></strong>
234 </article>
235 <?php endforeach; ?>
236 </div>
237 </section>
238
239 <section class="os-station-home__section" aria-labelledby="os-station-home-attention-heading">
240 <h2 id="os-station-home-attention-heading"><?php esc_html_e( 'Needs attention', 'desktop-mode' ); ?></h2>
241 <div class="os-station-home__attention">
242 <?php attention_rows( $snapshot['attention'] ); ?>
243 </div>
244 </section>
245
246 <?php if ( array() !== $snapshot['cardPreferences'] ) : ?>
247 <section class="os-station-home__section os-station-home__cards-section" aria-labelledby="os-station-home-cards-heading">
248 <div class="os-station-home__section-heading">
249 <h2 id="os-station-home-cards-heading"><?php esc_html_e( 'From your plugins', 'desktop-mode' ); ?></h2>
250 <os-button variant="ghost" size="sm" os-action="customize">
251 <span class="dashicons dashicons-admin-generic" aria-hidden="true"></span>
252 <?php esc_html_e( 'Customize', 'desktop-mode' ); ?>
253 </os-button>
254 </div>
255 <div class="os-station-home__cards">
256 <?php cards( $snapshot['cards'] ); ?>
257 </div>
258 </section>
259 <?php endif; ?>
260 </main>
261 </div>
262
263 <?php card_modal( (bool) $state->get( 'customizing' ), $snapshot['cardPreferences'] ); ?>
264 </div>
265 <?php
266 }
267
268 /**
269 * Continue working: the recent rows, or the clear-desk empty state.
270 *
271 * @param array[] $work Recent work items.
272 * @return void
273 */
274 function work( array $work ) {
275 if ( array() === $work ) {
276 ?>
277 <os-empty-state
278 icon="welcome-write-blog"
279 heading="<?php esc_attr_e( 'Your desk is clear', 'desktop-mode' ); ?>"
280 description="<?php esc_attr_e( 'Start something new and it will be waiting here when you return.', 'desktop-mode' ); ?>"
281 ></os-empty-state>
282 <?php
283 return;
284 }
285 foreach ( $work as $item ) {
286 ?>
287 <a class="os-station-home__work-row" os-key="<?php echo esc( $item['id'] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- esc() escapes. ?>" href="<?php echo esc_url( $item['editUrl'] ); ?>">
288 <span class="os-station-home__row-icon"><?php echo icon( $item['icon'] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- icon() escapes. ?></span>
289 <span class="os-station-home__row-copy">
290 <span class="os-station-home__row-title"><?php echo text( $item['title'] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- text() escapes. ?></span>
291 <span class="os-station-home__row-meta"><?php echo text( $item['typeLabel'] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- text() escapes. ?></span>
292 </span>
293 <os-badge tone="<?php echo esc( status_tone( $item['status'] ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- esc() escapes. ?>"><?php echo esc( $item['statusLabel'] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- esc() escapes. ?></os-badge>
294 <?php if ( '' !== $item['modifiedGmt'] ) : ?>
295 <os-relative-time datetime="<?php echo esc( $item['modifiedGmt'] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- esc() escapes. ?>" compact></os-relative-time>
296 <?php endif; ?>
297 <?php echo icon( 'dashicons-arrow-right-alt2' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- icon() escapes. ?>
298 </a>
299 <?php
300 }
301 }
302
303 /**
304 * Needs attention: the queue, or the explicit all-clear.
305 *
306 * @param array[] $attention Attention items.
307 * @return void
308 */
309 function attention_rows( array $attention ) {
310 if ( array() === $attention ) {
311 ?>
312 <div class="os-station-home__all-clear">
313 <?php echo icon( 'dashicons-yes-alt' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- icon() escapes. ?>
314 <span>
315 <strong><?php esc_html_e( 'All clear', 'desktop-mode' ); ?></strong>
316 <span><?php esc_html_e( 'Nothing needs your attention right now.', 'desktop-mode' ); ?></span>
317 </span>
318 </div>
319 <?php
320 return;
321 }
322 foreach ( $attention as $item ) {
323 ?>
324 <a class="os-station-home__attention-row" os-key="<?php echo esc( $item['id'] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- esc() escapes. ?>" href="<?php echo esc_url( $item['url'] ); ?>">
325 <span class="os-station-home__attention-count">
326 <?php echo icon( $item['icon'] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- icon() escapes. ?>
327 <strong><?php echo esc( number_format_i18n( $item['count'] ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- esc() escapes. ?></strong>
328 </span>
329 <span class="os-station-home__attention-copy">
330 <strong><?php echo text( $item['label'] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- text() escapes. ?></strong>
331 <span><?php echo text( $item['description'] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- text() escapes. ?></span>
332 </span>
333 <?php echo icon( 'dashicons-arrow-right-alt2' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- icon() escapes. ?>
334 </a>
335 <?php
336 }
337 }
338
339 /**
340 * From your plugins: the enabled cards, or the opt-in prompt.
341 *
342 * @param array[] $cards Enabled card payloads.
343 * @return void
344 */
345 function cards( array $cards ) {
346 if ( array() === $cards ) {
347 ?>
348 <div class="os-station-home__cards-empty">
349 <?php echo icon( 'dashicons-admin-plugins' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- icon() escapes. ?>
350 <span>
351 <strong><?php esc_html_e( 'Make this space yours', 'desktop-mode' ); ?></strong>
352 <span><?php esc_html_e( 'Use Customize to opt in to information from your plugins.', 'desktop-mode' ); ?></span>
353 </span>
354 </div>
355 <?php
356 return;
357 }
358 foreach ( $cards as $card ) {
359 $linked = '' !== (string) $card['url'];
360 $detail = '' !== (string) $card['detail'] ? $card['detail'] : $card['description'];
361 $inner = '<span class="os-station-home__card-head">'
362 . '<span class="os-station-home__card-icon">' . card_icon( $card['icon'] ) . '</span>'
363 . '<span><strong>' . text( $card['label'] ) . '</strong>'
364 . ( '' !== (string) $card['provider'] ? '<span>' . text( $card['provider'] ) . '</span>' : '' )
365 . '</span></span>';
366 if ( '' !== (string) $card['value'] ) {
367 $inner .= '<strong class="os-station-home__card-value">' . text( $card['value'] ) . '</strong>';
368 }
369 if ( '' !== (string) $detail ) {
370 $inner .= '<span class="os-station-home__card-detail">' . text( $detail ) . '</span>';
371 }
372 if ( $linked ) {
373 $label = '' !== (string) $card['actionLabel'] ? $card['actionLabel'] : __( 'Open', 'desktop-mode' );
374 $inner .= '<span class="os-station-home__card-action">' . text( $label ) . icon( 'dashicons-arrow-right-alt2' ) . '</span>';
375 }
376 echo tag( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- tag() escapes every attribute; the inner HTML is built above with the escaping helpers.
377 $linked ? 'a' : 'article',
378 array(
379 'class' => 'os-station-home__card',
380 'os-key' => $card['id'],
381 'data-tone' => $card['tone'],
382 'href' => $linked ? $card['url'] : false,
383 'target' => $linked && $card['external'] ? '_blank' : false,
384 'rel' => $linked && $card['external'] ? 'noopener' : false,
385 ),
386 $inner
387 );
388 }
389 }
390
391 /**
392 * The Customize modal: one switch per registered card. Rendered
393 * always, shown while `customizing` is set; dismissing it dispatches
394 * `customize_close` so the state agrees with what the user sees.
395 *
396 * @param bool $open Whether the modal is open.
397 * @param array[] $preferences Picker rows.
398 * @return void
399 */
400 function card_modal( $open, array $preferences ) {
401 ?>
402 <os-modal
403 class="os-station-home__card-modal"
404 title="<?php esc_attr_e( 'Customize Station Home', 'desktop-mode' ); ?>"
405 size="md"
406 os-action="customize_close" <?php echo esc_attr( $open ? 'open' : '' ); ?>>
407 <p class="os-station-home__card-modal-intro">
408 <?php esc_html_e( 'Choose which plugin cards can show information on your Station Home.', 'desktop-mode' ); ?>
409 </p>
410 <div class="os-station-home__card-preferences">
411 <?php
412 foreach ( $preferences as $preference ) {
413 echo tag( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- tag() escapes every attribute.
414 'os-switch',
415 array(
416 'os-key' => $preference['id'],
417 'value' => $preference['id'],
418 'label' => html_entity_decode( (string) $preference['label'], ENT_QUOTES | ENT_HTML5, 'UTF-8' ),
419 'description' => implode(
420 '',
421 array_filter(
422 array(
423 html_entity_decode( (string) $preference['provider'], ENT_QUOTES | ENT_HTML5, 'UTF-8' ),
424 html_entity_decode( (string) $preference['description'], ENT_QUOTES | ENT_HTML5, 'UTF-8' ),
425 )
426 )
427 ),
428 'block' => true,
429 'size' => 'sm',
430 'tone' => 'accent',
431 'checked' => (bool) $preference['enabled'],
432 'os-action' => 'toggle_card',
433 'os-arg-id' => $preference['id'],
434 )
435 );
436 }
437 ?>
438 </div>
439 </os-modal>
440 <?php
441 }
442