@fasl-work/caos-app-shell 0.3.0 → 0.6.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/README.md CHANGED
@@ -60,9 +60,20 @@ const config: ShellConfig = {
60
60
 
61
61
  ## Exports
62
62
 
63
- `AppShell`, `ThemeToggle`, `LanguageToggle`, `useThemeStore`, `applyTheme`, `readTheme`, `useLangStore`,
63
+ `AppShell`, `WorkbenchShell`, `ThemeToggle`, `LanguageToggle`, `useThemeStore`, `applyTheme`, `readTheme`, `useLangStore`,
64
64
  `useShellLang`, `usePausedViz`, `createVizLoop`, `CaseSelector` (+ `caseModel` helpers), `Tabs`, `SubTabs`,
65
65
  `Callout`, `Equation`, `InlineMath`, `Figure`, `CitationsProvider`, `Cite`, `Refs`, `ReferenceList`, plus
66
66
  the `@fasl-work/caos-app-shell/styles.css` design system.
67
67
 
68
+ ### Authenticated workbenches
69
+
70
+ `WorkbenchShell` is the shared frame for products whose authenticated UI needs a desktop sidebar and
71
+ mobile bottom navigation. It renders typed routes and the main landmark while the product supplies its
72
+ own stateful controls through `brand`, `sidebarFooter`, `headerLead`, `headerActions`, and `overlays`.
73
+ Authentication, account policy, preference persistence, chat, and modal content remain product-owned.
74
+ The component uses the stable core `NavLink` contract and supports `react-router` 6, 7, and 8.
75
+ Browser applications on 6/7 install the matching `react-router-dom`, which supplies the core peer;
76
+ Router 8 applications provide `react-router` directly. Do not mix different core and DOM majors in
77
+ one application.
78
+
68
79
  MIT · part of the Faena mining-analytics hub.
package/dist/index.d.ts CHANGED
@@ -10,7 +10,15 @@ interface ArchTab {
10
10
  /** the explanation body (paragraphs separated by a blank line). */
11
11
  body_en: string;
12
12
  body_es: string;
13
- /** an inline '<svg…>…</svg>' string OR a path under the app's public/ (fetched + inlined). */
13
+ /**
14
+ * an inline '<svg…>…</svg>' string OR a path under the app's public/ (fetched + inlined).
15
+ *
16
+ * ONE file carries BOTH languages (ADR-0058). Tag each translatable `<text>` twice at the same
17
+ * coordinates, `class="… l-en"` and `class="… l-es"`; the modal wrapper sets `data-arch-lang`
18
+ * and the shell stylesheet shows exactly one. Two files would be two things to keep in step, and
19
+ * the one that is not on screen is the one that goes stale. Anything language-neutral (a number,
20
+ * a file name, an identifier) needs no pair.
21
+ */
14
22
  svg: string;
15
23
  }
16
24
  interface ArchitectureConfig {
@@ -68,6 +76,36 @@ declare function AppShell({ config, children }: {
68
76
  children: ReactNode;
69
77
  }): react.JSX.Element;
70
78
 
79
+ /** One authenticated workbench destination rendered by the shared responsive navigation frame. */
80
+ interface WorkbenchRoute {
81
+ path: string;
82
+ label: ReactNode;
83
+ icon?: ReactNode;
84
+ end?: boolean;
85
+ ariaLabel?: string;
86
+ }
87
+ /**
88
+ * Slots owned by a product around the shared workbench layout.
89
+ *
90
+ * The shell deliberately does not own authentication, preferences, account state, chat, or modal
91
+ * content. Products supply those controls as slots while this package owns the frame, navigation
92
+ * semantics, responsive layout classes, and main-content landmark.
93
+ */
94
+ interface WorkbenchShellProps {
95
+ brand: ReactNode;
96
+ routes: WorkbenchRoute[];
97
+ navigationLabel: string;
98
+ children: ReactNode;
99
+ sidebarFooter?: ReactNode;
100
+ headerLead?: ReactNode;
101
+ headerActions?: ReactNode;
102
+ overlays?: ReactNode;
103
+ mainId?: string;
104
+ className?: string;
105
+ }
106
+ /** Shared CAOS frame for authenticated consoles with desktop sidebar and mobile bottom navigation. */
107
+ declare function WorkbenchShell({ brand, routes, navigationLabel, children, sidebarFooter, headerLead, headerActions, overlays, mainId, className, }: WorkbenchShellProps): react.JSX.Element;
108
+
71
109
  /** Light/dark icon toggle. Shows the icon of the theme you would switch TO. */
72
110
  declare function ThemeToggle(): react.JSX.Element;
73
111
 
@@ -316,4 +354,4 @@ declare function ReferenceList({ ids, heading }: {
316
354
  heading?: string;
317
355
  }): react.JSX.Element;
318
356
 
319
- export { AppShell, type ArchTab, type ArchitectureConfig, ArchitectureModal, Callout, type CaseDef, type CaseGroup, type CaseKind, CaseSelector, type CaseSelectorProps, type CaseSelectorText, type Citation, CitationsProvider, Cite, Equation, Figure, InlineMath, type Lang, LanguageToggle, type PausedVizController, ReferenceList, Refs, type ShellConfig, type ShellRoute, type SubTabDef, SubTabs, type TabDef, Tabs, type Theme, ThemeToggle, type UsePausedVizOptions, type VizFrame, type VizLoop, type VizLoopDeps, type VizLoopOptions, applyTheme, caseKind, caseTooltip, casesInSource, createVizLoop, groupByCategory, readCaseParam, readTheme, sourcesPresent, useLangStore, usePausedViz, useShellLang, useThemeStore, withCaseParam };
357
+ export { AppShell, type ArchTab, type ArchitectureConfig, ArchitectureModal, Callout, type CaseDef, type CaseGroup, type CaseKind, CaseSelector, type CaseSelectorProps, type CaseSelectorText, type Citation, CitationsProvider, Cite, Equation, Figure, InlineMath, type Lang, LanguageToggle, type PausedVizController, ReferenceList, Refs, type ShellConfig, type ShellRoute, type SubTabDef, SubTabs, type TabDef, Tabs, type Theme, ThemeToggle, type UsePausedVizOptions, type VizFrame, type VizLoop, type VizLoopDeps, type VizLoopOptions, type WorkbenchRoute, WorkbenchShell, type WorkbenchShellProps, applyTheme, caseKind, caseTooltip, casesInSource, createVizLoop, groupByCategory, readCaseParam, readTheme, sourcesPresent, useLangStore, usePausedViz, useShellLang, useThemeStore, withCaseParam };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createContext, useState, useRef, useEffect, useCallback, useId, useContext } from 'react';
2
- import { NavLink } from 'react-router-dom';
2
+ import { NavLink } from 'react-router';
3
3
  import { Sun, Moon, Languages, Boxes, Github, Globe, Briefcase, Info } from 'lucide-react';
4
4
  import { create } from 'zustand';
5
5
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
@@ -223,7 +223,7 @@ function ArchitectureModal({ config, onClose }) {
223
223
  t.id
224
224
  );
225
225
  }) }),
226
- /* @__PURE__ */ jsxs("div", { role: "tabpanel", style: { overflowY: "auto", padding: "14px 16px 18px" }, children: [
226
+ /* @__PURE__ */ jsxs("div", { role: "tabpanel", "data-arch-lang": es ? "es" : "en", style: { overflowY: "auto", padding: "14px 16px 18px" }, children: [
227
227
  body.map((p, i) => /* @__PURE__ */ jsx("p", { style: { color: "var(--color-fg)", fontSize: 13, lineHeight: 1.65, margin: i === 0 ? "0 0 10px" : "10px 0" }, children: p }, i)),
228
228
  err && /* @__PURE__ */ jsxs("div", { style: { color: "var(--color-warn, #f87171)", fontSize: 12 }, children: [
229
229
  "SVG: ",
@@ -308,6 +308,55 @@ function AppShell({ config, children }) {
308
308
  ] }) }) })
309
309
  ] });
310
310
  }
311
+ function WorkbenchShell({
312
+ brand,
313
+ routes,
314
+ navigationLabel,
315
+ children,
316
+ sidebarFooter,
317
+ headerLead,
318
+ headerActions,
319
+ overlays,
320
+ mainId = "main-content",
321
+ className
322
+ }) {
323
+ const rootClassName = ["caos-workbench-shell", "app-shell", className].filter(Boolean).join(" ");
324
+ return /* @__PURE__ */ jsxs("div", { className: rootClassName, children: [
325
+ /* @__PURE__ */ jsxs(
326
+ "aside",
327
+ {
328
+ className: "caos-workbench-sidebar sidebar",
329
+ style: { "--caos-workbench-route-count": routes.length },
330
+ children: [
331
+ brand,
332
+ /* @__PURE__ */ jsx("nav", { "aria-label": navigationLabel, children: routes.map((route) => /* @__PURE__ */ jsxs(
333
+ NavLink,
334
+ {
335
+ to: route.path,
336
+ end: route.end ?? route.path === "/",
337
+ "aria-label": route.ariaLabel,
338
+ className: ({ isActive }) => isActive ? "active" : void 0,
339
+ children: [
340
+ route.icon,
341
+ /* @__PURE__ */ jsx("span", { children: route.label })
342
+ ]
343
+ },
344
+ route.path
345
+ )) }),
346
+ sidebarFooter
347
+ ]
348
+ }
349
+ ),
350
+ /* @__PURE__ */ jsxs("div", { className: "caos-workbench-main shell-main", children: [
351
+ /* @__PURE__ */ jsxs("header", { className: "caos-workbench-header topbar", children: [
352
+ headerLead,
353
+ headerActions
354
+ ] }),
355
+ /* @__PURE__ */ jsx("main", { id: mainId, className: "caos-workbench-content content", children })
356
+ ] }),
357
+ overlays
358
+ ] });
359
+ }
311
360
 
312
361
  // src/lib/vizLoop.ts
313
362
  function createVizLoop(frame, options, deps) {
@@ -788,4 +837,4 @@ function ReferenceList({ ids, heading }) {
788
837
  ] });
789
838
  }
790
839
 
791
- export { AppShell, ArchitectureModal, Callout, CaseSelector, CitationsProvider, Cite, Equation, Figure, InlineMath, LanguageToggle, ReferenceList, Refs, SubTabs, Tabs, ThemeToggle, applyTheme, caseKind, caseTooltip, casesInSource, createVizLoop, groupByCategory, readCaseParam, readTheme, sourcesPresent, useLangStore, usePausedViz, useShellLang, useThemeStore, withCaseParam };
840
+ export { AppShell, ArchitectureModal, Callout, CaseSelector, CitationsProvider, Cite, Equation, Figure, InlineMath, LanguageToggle, ReferenceList, Refs, SubTabs, Tabs, ThemeToggle, WorkbenchShell, applyTheme, caseKind, caseTooltip, casesInSource, createVizLoop, groupByCategory, readCaseParam, readTheme, sourcesPresent, useLangStore, usePausedViz, useShellLang, useThemeStore, withCaseParam };
package/package.json CHANGED
@@ -1,56 +1,59 @@
1
- {
2
- "name": "@fasl-work/caos-app-shell",
3
- "version": "0.3.0",
4
- "description": "Shared web-app shell + content primitives + design system for the CAOS / Faena public apps (ADR-0016).",
5
- "license": "MIT",
6
- "author": "Felipe Santibáñez-Leal",
7
- "repository": {
8
- "type": "git",
9
- "url": "git+https://github.com/fsantibanezleal/CAOS_APP_SHELL.git"
10
- },
11
- "type": "module",
12
- "publishConfig": {
13
- "access": "public"
14
- },
15
- "sideEffects": [
16
- "*.css"
17
- ],
18
- "exports": {
19
- ".": {
20
- "types": "./dist/index.d.ts",
21
- "import": "./dist/index.js"
22
- },
23
- "./styles.css": "./styles.css"
24
- },
25
- "files": [
26
- "dist",
27
- "styles.css"
28
- ],
29
- "scripts": {
30
- "build": "tsup",
31
- "test": "node --import tsx --test test/vizLoop.test.ts test/caseModel.test.ts",
32
- "prepublishOnly": "npm run build"
33
- },
34
- "peerDependencies": {
35
- "katex": "^0.16.0",
36
- "lucide-react": ">=0.400.0",
37
- "react": "^18 || ^19",
38
- "react-dom": "^18 || ^19",
39
- "react-router-dom": "^6 || ^7",
40
- "zustand": "^4 || ^5"
41
- },
42
- "devDependencies": {
43
- "@types/katex": "^0.16.7",
44
- "@types/react": "^19.0.0",
45
- "@types/react-dom": "^19.0.0",
46
- "katex": "^0.16.11",
47
- "lucide-react": "^0.469.0",
48
- "react": "^19.0.0",
49
- "react-dom": "^19.0.0",
50
- "react-router-dom": "^7.1.1",
51
- "tsup": "^8.3.5",
52
- "tsx": "^4.23.0",
53
- "typescript": "^5.7.3",
54
- "zustand": "^5.0.2"
55
- }
56
- }
1
+ {
2
+ "name": "@fasl-work/caos-app-shell",
3
+ "version": "0.6.0",
4
+ "description": "Shared web-app shell + content primitives + design system for the CAOS / Faena public apps (ADR-0016).",
5
+ "license": "MIT",
6
+ "author": "Felipe Santibáñez-Leal",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/fsantibanezleal/CAOS_APP_SHELL.git"
10
+ },
11
+ "type": "module",
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "sideEffects": [
16
+ "*.css"
17
+ ],
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "import": "./dist/index.js"
22
+ },
23
+ "./styles.css": "./styles.css"
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "styles.css"
28
+ ],
29
+ "scripts": {
30
+ "build": "tsup",
31
+ "test": "node --import tsx --test \"test/**/*.test.ts\" \"test/**/*.test.tsx\"",
32
+ "prepublishOnly": "npm run build"
33
+ },
34
+ "peerDependencies": {
35
+ "katex": "^0.16.0",
36
+ "lucide-react": ">=0.400.0",
37
+ "react": "^18 || ^19",
38
+ "react-dom": "^18 || ^19",
39
+ "react-router": "^6 || ^7 || ^8",
40
+ "zustand": "^4 || ^5"
41
+ },
42
+ "devDependencies": {
43
+ "@types/katex": "^0.16.7",
44
+ "@types/react": "^19.0.0",
45
+ "@types/react-dom": "^19.0.0",
46
+ "katex": "^0.16.11",
47
+ "lucide-react": "^0.469.0",
48
+ "react": "^19.0.0",
49
+ "react-dom": "^19.0.0",
50
+ "react-router": "^8.3.0",
51
+ "tsup": "^8.3.5",
52
+ "tsx": "^4.23.0",
53
+ "typescript": "^5.7.3",
54
+ "zustand": "^5.0.2"
55
+ },
56
+ "overrides": {
57
+ "esbuild": "0.28.1"
58
+ }
59
+ }
package/styles.css CHANGED
@@ -25,7 +25,8 @@
25
25
  --color-shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 6px 16px rgba(0, 0, 0, 0.45);
26
26
  --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
27
27
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
28
- --maxw: 1200px;
28
+ --maxw: 1200px; /* reading width, for PROSE pages */
29
+ --maxw-wide: 100%; /* instrument width, for workbench pages */
29
30
  }
30
31
 
31
32
  [data-theme="light"] {
@@ -69,6 +70,10 @@ code, kbd { font-family: var(--font-mono); }
69
70
  .container { width: 100%; max-width: var(--maxw); margin-inline: auto; padding-inline: 1.5rem; }
70
71
  .page { flex: 1 1 auto; width: 100%; }
71
72
  .page-body { width: 100%; max-width: var(--maxw); margin-inline: auto; padding: 2.25rem 1.5rem 3rem; }
73
+ /* A WORKBENCH is not prose. Capping an instrument at a 1200px reading measure wastes every pixel beyond
74
+ it, which is why the App surfaces across the line render their visualization as a thumbnail on a wide
75
+ display. Prose pages keep the reading width; the App route opts into the full viewport. */
76
+ .page-body.wide { max-width: var(--maxw-wide); padding-inline: 1.25rem; }
72
77
  .muted { color: var(--color-fg-subtle); }
73
78
  .faint { color: var(--color-fg-faint); }
74
79
  .mono { font-family: var(--font-mono); font-size: 0.9em; }
@@ -155,7 +160,18 @@ code, kbd { font-family: var(--font-mono); }
155
160
  .callout-body p:last-child { margin-bottom: 0; }
156
161
 
157
162
  /* ── Tabs ── */
158
- .tablist { display: flex; gap: 0.35rem; padding-bottom: 0.6rem; margin-bottom: 1.5rem; border-bottom: 1px solid var(--color-border); flex-wrap: wrap; }
163
+ .tablist { display: flex; gap: 0.35rem; padding-bottom: 0.6rem; margin-bottom: 1.5rem; border-bottom: 1px solid var(--color-border); flex-wrap: nowrap; }
164
+ /* SINGLE-LINE TAB BAR.
165
+ `flex-wrap: wrap` lets a 14-to-18 tab bar spill onto a second and third row, and every extra row is
166
+ vertical space taken from the instrument below it, permanently, on every render. A tab bar is
167
+ navigation chrome: it gets ONE row and scrolls horizontally when it does not fit.
168
+ Scrollbar is hidden but the row stays scrollable by wheel, trackpad, drag and keyboard. */
169
+ .tablist { flex-wrap: nowrap !important; overflow-x: auto; overflow-y: hidden;
170
+ scrollbar-width: none; -ms-overflow-style: none; padding-bottom: 0.4rem; margin-bottom: 0.9rem;
171
+ scroll-behavior: smooth; }
172
+ .tablist::-webkit-scrollbar { display: none; }
173
+ .tablist .tab, .tablist button { flex: 0 0 auto; white-space: nowrap; }
174
+
159
175
  .tab { padding: 0.5rem 1rem; border: 1px solid transparent; border-radius: 8px; background: transparent; color: var(--color-fg-subtle); font-size: 0.92rem; font-weight: 550; transition: color 0.15s, background-color 0.15s, border-color 0.15s; }
160
176
  .tab:hover { color: var(--color-fg); background: var(--color-surface-2); }
161
177
  .tab.active { color: var(--color-accent); background: var(--color-accent-soft); border-color: color-mix(in oklab, var(--color-accent) 35%, transparent); font-weight: 650; }
@@ -240,3 +256,56 @@ code, kbd { font-family: var(--font-mono); }
240
256
  .cs-kind-uploaded { color: var(--color-warn); border-color: color-mix(in oklab, var(--color-warn) 40%, transparent); }
241
257
  .cs-modified { display: flex; align-items: center; gap: 0.5rem; }
242
258
  .cs-reset { border: none; background: transparent; color: var(--color-accent); font-size: 0.8rem; font-weight: 600; padding: 0.1rem 0.2rem; text-decoration: underline; text-underline-offset: 2px; }
259
+
260
+ /* Authenticated workbench frame. Product styles may refine tokens and controls, but the package owns
261
+ the responsive navigation/layout contract and keeps route/content landmarks consistent. */
262
+ .caos-workbench-shell { min-height: 100vh; display: grid; grid-template-columns: 238px minmax(0, 1fr); }
263
+ .caos-workbench-sidebar { position: fixed; inset: 0 auto 0 0; z-index: 20; width: 238px; display: flex; flex-direction: column; padding: 1.25rem 0.875rem; border-right: 1px solid var(--color-border); background: var(--color-surface); }
264
+ .caos-workbench-sidebar nav { display: grid; gap: 0.25rem; }
265
+ .caos-workbench-sidebar nav a { display: flex; align-items: center; gap: 0.75rem; padding: 0.625rem 0.7rem; border: 1px solid transparent; border-radius: 0.7rem; color: var(--color-fg-subtle); font-size: 0.9rem; font-weight: 650; }
266
+ .caos-workbench-sidebar nav a:hover { color: var(--color-fg); background: var(--color-surface-2); text-decoration: none; }
267
+ .caos-workbench-sidebar nav a.active { color: var(--color-accent); border-color: color-mix(in oklab, var(--color-accent) 24%, transparent); background: var(--color-accent-soft); }
268
+ .caos-workbench-main { grid-column: 2; min-width: 0; }
269
+ .caos-workbench-header { position: sticky; top: 0; z-index: 15; min-height: 64px; display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 0 2rem; border-bottom: 1px solid var(--color-border); background: color-mix(in oklab, var(--color-bg) 88%, transparent); backdrop-filter: blur(12px); }
270
+ .caos-workbench-content { width: min(1540px, 100%); margin: 0 auto; padding: 2.25rem 2rem 4rem; }
271
+
272
+ @media (max-width: 760px) {
273
+ .caos-workbench-shell { display: block; padding-bottom: 66px; }
274
+ .caos-workbench-sidebar { inset: auto 0 0; width: auto; height: 62px; display: block; border: 0; border-top: 1px solid var(--color-border); padding: 5px 8px; }
275
+ .caos-workbench-sidebar > :not(nav) { display: none; }
276
+ .caos-workbench-sidebar nav { grid-template-columns: repeat(var(--caos-workbench-route-count, 5), minmax(0, 1fr)); }
277
+ .caos-workbench-sidebar nav a { display: grid; justify-items: center; gap: 2px; overflow: hidden; padding: 5px; font-size: 0.7rem; text-overflow: ellipsis; white-space: nowrap; }
278
+ .caos-workbench-main { min-width: 0; }
279
+ .caos-workbench-header { min-height: 58px; padding: 0 1rem; }
280
+ .caos-workbench-content { padding: 1.5rem 1rem 2.5rem; }
281
+ }
282
+
283
+ /* CONTAINED LAYOUT. A `nowrap` tab bar sizes its container to its intrinsic width unless every
284
+ ancestor is allowed to shrink: a flex/grid item defaults to `min-width: auto`. Without this, making
285
+ the tab bar a single scrollable row pushes the whole page wider than the viewport and the user has to
286
+ drag sideways to reach the right edge. Measured on ChargeCascade: scrollWidth 1817 on a 1600 viewport. */
287
+ html, body { overflow-x: hidden; }
288
+ .page-body { max-width: 100%; }
289
+ .tabs, .tabpanel { min-width: 0; max-width: 100%; }
290
+ .tablist { min-width: 0; max-width: 100%; width: 100%; }
291
+
292
+ /* NO MAGIC CHROME NUMBER. An app surface must not compute its height from a hardcoded guess at the
293
+ header and footer: the guess drifts the moment either changes, leaving either a dead gap above the
294
+ footer or a few pixels of scroll. Measured on ChargeCascade: guess 150px, real 175px (header 57 +
295
+ footer 70 + 48px body padding), so 25px of scroll and a 48px gap. Apps that fill the viewport add
296
+ `.app-shell.fixed` and let flex distribute the height. */
297
+ .app-shell.fixed { height: 100dvh; min-height: 100dvh; overflow: hidden; }
298
+ .app-shell.fixed > .page { flex: 1 1 auto; min-height: 0; display: flex; }
299
+ .app-shell.fixed .page-body.wide { flex: 1 1 auto; min-height: 0; padding-top: 1rem; padding-bottom: 1rem; }
300
+
301
+ /* A viewport-filling app must not carry the prose footer margin: it becomes dead space between the
302
+ content and the chrome. Measured on ChargeCascade as exactly 48px (3rem). */
303
+ .app-shell.fixed .site-footer { margin-top: 0; }
304
+
305
+ /* ADR-0058 diagram i18n. A hand-authored architecture SVG carries BOTH languages inside ONE file,
306
+ and the modal wrapper picks one. Two files would be two things to keep in step, and the one not
307
+ on screen is the one that goes stale. The rules are scoped to [data-arch-lang] so the short class
308
+ names cannot collide with an app's own styles. */
309
+ [data-arch-lang] .l-es { display: none; }
310
+ [data-arch-lang="es"] .l-en { display: none; }
311
+ [data-arch-lang="es"] .l-es { display: inline; }