keystone_ui-pages 0.3.0 → 0.4.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.
@@ -22,7 +22,10 @@ module KeystoneUi
22
22
 
23
23
  def field_value
24
24
  value = params.to_unsafe_h["value"]
25
- value.is_a?(Array) ? value.map { |entry| entry.to_h.stringify_keys } : value.to_s
25
+ return value.map { |entry| entry.to_h.stringify_keys } if value.is_a?(Array)
26
+ return { "html" => value["html"].to_s } if value.is_a?(Hash) && value.key?("html")
27
+
28
+ value.to_s
26
29
  end
27
30
  end
28
31
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KeystoneUi
4
+ module Pages
5
+ module Manage
6
+ class TitlesController < BaseController
7
+ include EditsPages
8
+
9
+ def update
10
+ changed(Operations.title(page.document, params[:title]))
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -9,6 +9,8 @@ module KeystoneUi
9
9
  WIDE_SPANS = { "1/1" => "@sm:col-span-12", "3/4" => "@sm:col-span-9", "2/3" => "@sm:col-span-8", "1/2" => "@sm:col-span-6", "1/3" => "@sm:col-span-4", "1/4" => "@sm:col-span-3" }.freeze
10
10
  ELEMENT_ALIGNMENTS = { "stretch" => "flex flex-col", "start" => "flex justify-start", "center" => "flex justify-center", "end" => "flex justify-end" }.freeze
11
11
  COLUMN_ALIGNMENTS = { "top" => "justify-start", "center" => "justify-center", "bottom" => "justify-end" }.freeze
12
+ SIZED_SECTION_PADDING = "py-[calc(var(--ks-spacing)*16)] lg:py-[calc(var(--ks-spacing)*24)]"
13
+ SHADED_CLASSES = "bg-(color:--ks-color-fill-muted) dark:bg-(color:--ks-color-fill-muted-dark)"
12
14
  PLAIN_FIGURE_DIVIDER = "@sm:[.ks-pages-column:has(.ks-metric-plain)+&:has(.ks-metric-plain)]:border-l-(length:--ks-border-width) " \
13
15
  "@sm:[.ks-pages-column:has(.ks-metric-plain)+&:has(.ks-metric-plain)]:border-(color:--ks-color-border) " \
14
16
  "dark:@sm:[.ks-pages-column:has(.ks-metric-plain)+&:has(.ks-metric-plain)]:border-(color:--ks-color-border-dark)"
@@ -18,6 +20,12 @@ module KeystoneUi
18
20
  Rails.cache.fetch(key) { drawn_document(version.document, values: values).to_str }.html_safe
19
21
  end
20
22
 
23
+ def pages_paragraph(text:)
24
+ return ui_paragraph(text: text) unless text.is_a?(Hash)
25
+
26
+ rich_paragraphs(text["html"].to_s)
27
+ end
28
+
21
29
  def drawn_document(document, editing: false, values: {})
22
30
  @drawn_values = values.to_h.stringify_keys
23
31
  safe_join(document["sections"].map { |section| drawn_section(section, editing) })
@@ -25,12 +33,31 @@ module KeystoneUi
25
33
 
26
34
  private
27
35
 
36
+ def rich_paragraphs(html)
37
+ stored = Nokogiri::HTML5.fragment(html)
38
+ stored.css("script, style").each(&:remove)
39
+ fragment = Nokogiri::HTML5.fragment(Rails::HTML5::SafeListSanitizer.new.sanitize(stored.to_html, tags: %w[p br a], attributes: %w[href]))
40
+ fragment.css("a:not([href])").each { |link| link.replace(link.children) }
41
+ fragment.css("p").each { |paragraph| paragraph["class"] = "ks-paragraph" }
42
+ fragment.css("a[href^='http://'], a[href^='https://']").each { |link| link["target"], link["rel"] = "_blank", "noopener noreferrer" }
43
+ fragment.to_html.html_safe
44
+ end
45
+
28
46
  def drawn_section(section, editing)
29
47
  return if hidden?(section) && !editing
30
48
 
31
49
  spacing = Keystone::Ui::SectionComponent::SPACING_CLASSES.fetch((section["spacing"] || Operations::DEFAULTS[:section]["spacing"]).to_sym)
32
50
  data = editing ? { section_id: section["id"], hidden: hidden?(section) || nil }.compact : {}
33
- tag.section(safe_join(section["rows"].map { |row| drawn_row(row, editing) }), class: "ks-pages-section flex flex-col gap-6 print:break-inside-avoid #{spacing}", data: data)
51
+ shading = SHADED_CLASSES if section["background"] == "shaded"
52
+ tag.section(section_content(section, editing), class: [ "ks-pages-section flex flex-col gap-6 print:break-inside-avoid #{spacing}", shading ].compact.join(" "), data: data)
53
+ end
54
+
55
+ def section_content(section, editing)
56
+ rows = safe_join(section["rows"].map { |row| drawn_row(row, editing) })
57
+ width = section["width"]
58
+ return rows if width.nil? || width == "none"
59
+
60
+ tag.div(rows, class: "ks-page #{Keystone::Ui::PageComponent::MAX_WIDTH_CLASSES.fetch(width.to_sym)} w-full flex flex-col gap-6 #{SIZED_SECTION_PADDING}")
34
61
  end
35
62
 
36
63
  def drawn_row(row, editing)
@@ -75,6 +102,7 @@ module KeystoneUi
75
102
  return [] if (field[:kind] == :list || (field[:kind] == :data && field[:default].is_a?(Array))) && value.blank?
76
103
  return nil if field[:kind] == :data && value.blank?
77
104
  return value if value.blank?
105
+ return ERB::Util.html_escape(value) if field[:as] == :escaped_text
78
106
  return value.to_sym if field[:as] == :key
79
107
  return value.to_s == "true" if field[:kind] == :yes_or_no
80
108
  return value.to_sym if field[:kind] == :one_of
@@ -91,7 +119,7 @@ module KeystoneUi
91
119
  end
92
120
 
93
121
  def filled(stored)
94
- return stored.presence unless stored.is_a?(Hash)
122
+ return stored.presence unless stored.is_a?(Hash) && stored.key?("value")
95
123
 
96
124
  (@drawn_values || {})[stored["value"]].presence || stored["fallback"].presence || ""
97
125
  end
@@ -23,6 +23,7 @@ module KeystoneUi
23
23
  samples: Values.samples_for(page),
24
24
  sample: chosen_sample(page),
25
25
  samples_url: manage_page_sample_path(page),
26
+ title_url: manage_page_title_path(page),
26
27
  access_exceptions: KeystoneUi::Pages.access_exceptions.invert.to_a,
27
28
  access_exception: page.access_exception,
28
29
  access_exception_url: manage_page_access_exception_path(page),
@@ -6,6 +6,7 @@ import Checkbox from "keystone_ui-react/src/Checkbox.jsx"
6
6
  import Button from "keystone_ui-react/src/Button.jsx"
7
7
  import { Label } from "keystone_ui-react/src/FieldText.jsx"
8
8
  import ListField from "./ListField"
9
+ import RichTextField from "./RichTextField"
9
10
 
10
11
  const FieldInput = ({ field, value, onChange }) => {
11
12
  const shown = value ?? field.default ?? ""
@@ -59,7 +60,9 @@ export default function ElementFields({ element, definition, alignments, valueNa
59
60
  {field.kind === "list" && !isBound(element.fields[field.key]) &&
60
61
  <ListField field={field} entries={element.fields[field.key] ?? field.default ?? []} onChange={onChange} />}
61
62
  {field.kind === "data" && <p className="ks-hint">Filled with data the host supplies on a template page</p>}
62
- {field.kind !== "list" && field.kind !== "data" && <FieldInput key={JSON.stringify(element.fields[field.key] ?? null)} field={field}
63
+ {field.kind === "rich_text" &&
64
+ <RichTextField key={JSON.stringify(element.fields[field.key] ?? null)} field={field} value={element.fields[field.key]} onChange={onChange} />}
65
+ {field.kind !== "list" && field.kind !== "data" && field.kind !== "rich_text" && <FieldInput key={JSON.stringify(element.fields[field.key] ?? null)} field={field}
63
66
  value={isBound(element.fields[field.key]) ? element.fields[field.key].fallback ?? "" : element.fields[field.key]}
64
67
  onChange={(key, text) => (isBound(element.fields[key]) ? onBind(key, element.fields[key].value, text) : onChange(key, text))} />}
65
68
  {isBound(element.fields[field.key]) &&
@@ -5,7 +5,7 @@ import Alert from "keystone_ui-react/src/Alert.jsx"
5
5
  import { Label } from "keystone_ui-react/src/FieldText.jsx"
6
6
 
7
7
  const LABELS = {
8
- section: { spacing: "Section spacing" },
8
+ section: { spacing: "Section spacing", background: "Section background", width: "Section width" },
9
9
  row: { gap: "Row gap" },
10
10
  column: { wide: "Width on wide screens", narrow: "Width on narrow screens", align: "Vertical alignment" }
11
11
  }
@@ -2,13 +2,14 @@ import React, { useEffect, useLayoutEffect, useRef, useState } from "react"
2
2
  import { DragDropManager, Draggable, Droppable, KeyboardSensor, PointerSensor } from "@dnd-kit/dom"
3
3
  import { Sortable } from "@dnd-kit/dom/sortable"
4
4
  import Button from "keystone_ui-react/src/Button.jsx"
5
- import { addColumn, addElement, addRow, addSection, arrange, bindField, chooseAccessException, chooseSample, duplicateNode, moveNode, removeNode, setField, showWhen, stepHistory } from "./operations"
5
+ import { addColumn, addElement, addRow, addSection, arrange, bindField, chooseAccessException, chooseSample, duplicateNode, moveNode, removeNode, setField, setTitle, showWhen, stepHistory } from "./operations"
6
6
  import LayoutPanel from "./LayoutPanel"
7
7
  import DeviceWidth, { DEVICES } from "./DeviceWidth"
8
8
  import SelectionToolbar from "./SelectionToolbar"
9
9
  import ElementPalette from "./ElementPalette"
10
10
  import SelectionPath from "./SelectionPath"
11
11
  import SampleChoice from "./SampleChoice"
12
+ import PageTitle from "./PageTitle"
12
13
  import AccessExceptionChoice from "./AccessExceptionChoice"
13
14
  import ShowWhen from "./ShowWhen"
14
15
  import ThemeMode from "./ThemeMode"
@@ -29,7 +30,7 @@ const innermost = (target) => NODES.map(([selector, kind]) => [target.closest(se
29
30
 
30
31
  const PLACED_SENSORS = [PointerSensor.configure({ preventActivation: () => false }), KeyboardSensor]
31
32
 
32
- export default function PageEditor({ name, publish_url, unpublish_url, elements, canvas: drawn, document: saved, elements_url, sections_url, columns_url, rows_url, nodes_url, arrangements, defaults, shares, value_names, looks = [], samples = [], sample, samples_url, access_exceptions = [], access_exception, access_exception_url, history_url, live_url, back_url, undo_url, redo_url, undoable: couldUndo, redoable: couldRedo, token }) {
33
+ export default function PageEditor({ name, publish_url, unpublish_url, elements, canvas: drawn, document: saved, elements_url, sections_url, columns_url, rows_url, nodes_url, arrangements, defaults, shares, value_names, looks = [], samples = [], sample, samples_url, title_url, access_exceptions = [], access_exception, access_exception_url, history_url, live_url, back_url, undo_url, redo_url, undoable: couldUndo, redoable: couldRedo, token }) {
33
34
  const [canvas, setCanvas] = useState(drawn)
34
35
  const [undoable, setUndoable] = useState(couldUndo)
35
36
  const [redoable, setRedoable] = useState(couldRedo)
@@ -245,6 +246,7 @@ export default function PageEditor({ name, publish_url, unpublish_url, elements,
245
246
  <aside data-settings-panel className="w-72 shrink-0 overflow-y-auto border-l border-(--ks-color-border) bg-(--ks-color-surface) p-4 dark:border-(--ks-color-border-dark) dark:bg-(--ks-color-surface-dark)">
246
247
  <SelectionPath path={selectionPath} onChoose={choosePart} />
247
248
  {!node && !definition && <p className="ks-hint">Select something on the page to change it.</p>}
249
+ {!node && !definition && <PageTitle key={document.title ?? ""} title={document.title} onChange={(title) => setTitle(title_url, token, title).then(shown, refused)} />}
248
250
  {node && <LayoutPanel node={node} values={inUse(node)} arrangements={arrangements}
249
251
  onArrange={(id, setting, value) => arrange(nodes_url, token, id, setting, value).then(shown, refused)}
250
252
  refusal={refusal}
@@ -0,0 +1,16 @@
1
+ import React from "react"
2
+ import Input from "keystone_ui-react/src/Input.jsx"
3
+ import { Label } from "keystone_ui-react/src/FieldText.jsx"
4
+
5
+ export default function PageTitle({ title, onChange }) {
6
+ const shown = title ?? ""
7
+
8
+ return (
9
+ <div className="flex flex-col gap-1">
10
+ <Label htmlFor="ks-pages-page-title">Page title</Label>
11
+ <Input id="ks-pages-page-title" type="text" defaultValue={shown}
12
+ onBlur={(event) => event.target.value !== shown && onChange(event.target.value)} />
13
+ <p className="ks-hint">Shown in the browser tab once the page is published</p>
14
+ </div>
15
+ )
16
+ }
@@ -0,0 +1,100 @@
1
+ import React, { useEffect, useState } from "react"
2
+ import Input from "keystone_ui-react/src/Input.jsx"
3
+ import Button from "keystone_ui-react/src/Button.jsx"
4
+ import { Label } from "keystone_ui-react/src/FieldText.jsx"
5
+ import { LexicalComposer } from "@lexical/react/LexicalComposer"
6
+ import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin"
7
+ import { ContentEditable } from "@lexical/react/LexicalContentEditable"
8
+ import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary"
9
+ import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin"
10
+ import { LinkPlugin } from "@lexical/react/LexicalLinkPlugin"
11
+ import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"
12
+ import { LinkNode, TOGGLE_LINK_COMMAND, $isLinkNode } from "@lexical/link"
13
+ import { $generateHtmlFromNodes, $generateNodesFromDOM } from "@lexical/html"
14
+ import { $getRoot, $getSelection, $isRangeSelection, $createParagraphNode, $createTextNode, TextNode } from "lexical"
15
+
16
+ const filled = (stored) => (editor) => {
17
+ const root = $getRoot()
18
+ if (stored && typeof stored === "object" && "html" in stored) {
19
+ const dom = new DOMParser().parseFromString(stored.html, "text/html")
20
+ root.append(...$generateNodesFromDOM(editor, dom))
21
+ } else {
22
+ root.append($createParagraphNode().append($createTextNode(stored ?? "")))
23
+ }
24
+ }
25
+
26
+ const selectedLink = () => {
27
+ const selection = $getSelection()
28
+ if (!$isRangeSelection(selection)) return null
29
+ const node = selection.anchor.getNode()
30
+ return $isLinkNode(node) ? node : $isLinkNode(node.getParent()) ? node.getParent() : null
31
+ }
32
+
33
+ const PlainWords = () => {
34
+ const [editor] = useLexicalComposerContext()
35
+ useEffect(() => editor.registerNodeTransform(TextNode, (node) => {
36
+ if (node.getFormat() !== 0) node.setFormat(0)
37
+ if (node.getStyle() !== "") node.setStyle("")
38
+ }), [editor])
39
+ return null
40
+ }
41
+
42
+ const LinkControls = () => {
43
+ const [editor] = useLexicalComposerContext()
44
+ const [address, setAddress] = useState(null)
45
+
46
+ const opened = () => editor.getEditorState().read(() => setAddress(selectedLink()?.getURL() ?? ""))
47
+ const saved = () => {
48
+ editor.dispatchCommand(TOGGLE_LINK_COMMAND, address || null)
49
+ setAddress(null)
50
+ }
51
+ const removed = () => {
52
+ editor.dispatchCommand(TOGGLE_LINK_COMMAND, null)
53
+ setAddress(null)
54
+ }
55
+
56
+ if (address === null) return <Button type="button" variant="secondary" onClick={opened}>Link</Button>
57
+
58
+ return (
59
+ <div className="flex flex-col gap-2">
60
+ <Label htmlFor="ks-pages-link-address">Link address</Label>
61
+ <Input id="ks-pages-link-address" type="url" value={address} onChange={(event) => setAddress(event.target.value)} />
62
+ <div className="flex gap-2">
63
+ <Button type="button" onClick={saved}>Save link</Button>
64
+ <Button type="button" variant="secondary" onClick={removed}>Remove link</Button>
65
+ </div>
66
+ </div>
67
+ )
68
+ }
69
+
70
+ const SavedOnLeave = ({ fieldKey, onChange, children }) => {
71
+ const [editor] = useLexicalComposerContext()
72
+ const left = (event) => {
73
+ if (event.currentTarget.contains(event.relatedTarget)) return
74
+ editor.getEditorState().read(() => onChange(fieldKey, { html: $generateHtmlFromNodes(editor, null) }))
75
+ }
76
+ return <div data-rich-text-field={fieldKey} onBlur={left} className="flex flex-col gap-2">{children}</div>
77
+ }
78
+
79
+ export default function RichTextField({ field, value, onChange }) {
80
+ const config = {
81
+ namespace: `ks-pages-${field.key}`,
82
+ nodes: [LinkNode],
83
+ editorState: filled(value ?? field.default),
84
+ onError: (error) => { throw error }
85
+ }
86
+
87
+ return (
88
+ <LexicalComposer initialConfig={config}>
89
+ <SavedOnLeave fieldKey={field.key} onChange={onChange}>
90
+ <RichTextPlugin
91
+ contentEditable={<ContentEditable id={`ks-pages-field-${field.key}`} className="ks-input min-h-24" />}
92
+ ErrorBoundary={LexicalErrorBoundary} />
93
+ <HistoryPlugin />
94
+ <LinkPlugin />
95
+ <PlainWords />
96
+ <LinkControls />
97
+ </SavedOnLeave>
98
+ </LexicalComposer>
99
+ )
100
+ }
@@ -7,6 +7,8 @@ const sent = (url, token, method, body = {}) =>
7
7
 
8
8
  export const addElement = (url, token, key, columnId) => sent(url, token, "POST", { key, column_id: columnId })
9
9
 
10
+ export const setTitle = (url, token, title) => sent(url, token, "POST", { title })
11
+
10
12
  export const setField = (url, token, elementId, field, value) => sent(`${url}/${elementId}`, token, "PATCH", { field, value })
11
13
 
12
14
  export const addSection = (url, token) => sent(url, token, "POST", {})
@@ -8,6 +8,10 @@ module KeystoneUi
8
8
  enum :status, { live: "live", superseded: "superseded" }
9
9
 
10
10
  belongs_to :page, class_name: "KeystoneUi::Pages::Page", inverse_of: :versions
11
+
12
+ def title
13
+ document["title"]
14
+ end
11
15
  end
12
16
  end
13
17
  end
@@ -1,3 +1,4 @@
1
+ <% content_for(:title, @version.title) if @version.title %>
1
2
  <div data-live-page class="@container print:bg-white print:[print-color-adjust:exact]">
2
3
  <%= drawn_version(@version, values: @values.to_h) %>
3
4
  </div>
data/config/routes.rb CHANGED
@@ -20,6 +20,7 @@ KeystoneUi::Pages::Engine.routes.draw do
20
20
  end
21
21
  resource :document, only: %i[update]
22
22
  post :sample, to: "samples#update"
23
+ post :title, to: "titles#update"
23
24
  post :access_exception, to: "access_exceptions#update"
24
25
  post :undo, to: "edit_history#undo"
25
26
  post :redo, to: "edit_history#redo"
@@ -23,8 +23,8 @@ module KeystoneUi
23
23
  { key: :note, label: "Note", kind: :text } ])
24
24
  catalog.register(key: :label, name: "Label", component: :ui_caption, icon: :tag, group: "Text",
25
25
  fields: [ { key: :text, label: "Text", kind: :text, default: "Label" } ])
26
- catalog.register(key: :paragraph, name: "Paragraph", component: :ui_paragraph, icon: :text, group: "Text",
27
- fields: [ { key: :text, label: "Text", kind: :long_text, default: "Write the text of this paragraph here." } ])
26
+ catalog.register(key: :paragraph, name: "Paragraph", component: :pages_paragraph, icon: :text, group: "Text",
27
+ fields: [ { key: :text, label: "Text", kind: :rich_text, default: "Write the text of this paragraph here." } ])
28
28
  catalog.register(key: :hero, name: "Hero", component: :ui_hero, icon: :heading, group: "Text",
29
29
  fields: [ { key: :title, label: "Title", kind: :text, default: "Headline" },
30
30
  { key: :subtitle, label: "Subtitle", kind: :long_text },
@@ -66,7 +66,7 @@ module KeystoneUi
66
66
  { key: :subtitle, label: "Subtitle", kind: :text },
67
67
  { key: :features, label: "Features", kind: :list,
68
68
  entry: [ { key: :title, label: "Title", kind: :text }, { key: :description, label: "Description", kind: :long_text },
69
- { key: :icon, label: "Icon", kind: :text, hidden: true,
69
+ { key: :icon, label: "Icon", kind: :text, as: :escaped_text,
70
70
  default: '<svg viewBox="0 0 24 24" class="size-6 fill-none stroke-current stroke-2"><path d="M5 12l5 5L20 7"/></svg>' } ],
71
71
  default: [ { "title" => "Fast", "description" => "Pages load quickly." },
72
72
  { "title" => "Clear", "description" => "Every page reads plainly." },
@@ -14,13 +14,13 @@ module KeystoneUi
14
14
  module Operations
15
15
  WIDTHS = %w[1/1 3/4 2/3 1/2 1/3 1/4].freeze
16
16
  ARRANGEMENTS = {
17
- section: { "spacing" => %w[sm md lg] },
17
+ section: { "spacing" => %w[sm md lg], "background" => %w[plain shaded], "width" => %w[none sm md lg xl] },
18
18
  row: { "gap" => %w[sm md lg xl] },
19
19
  column: { "wide" => WIDTHS, "narrow" => WIDTHS, "align" => %w[top center bottom] },
20
20
  element: { "align" => %w[stretch start center end] }
21
21
  }.freeze
22
22
  DEFAULTS = {
23
- section: { "spacing" => "md" },
23
+ section: { "spacing" => "md", "background" => "plain", "width" => "none" },
24
24
  row: { "gap" => "md" },
25
25
  column: { "narrow" => "1/1", "align" => "top" },
26
26
  element: { "align" => "stretch" }
@@ -36,6 +36,10 @@ module KeystoneUi
36
36
  end
37
37
  end
38
38
 
39
+ def title(document, text)
40
+ copied(document).tap { |changed| changed["title"] = text.to_s.strip.presence }
41
+ end
42
+
39
43
  def add_section(document)
40
44
  copied(document).tap { |changed| changed["sections"] << Document.section }
41
45
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module KeystoneUi
4
4
  module Pages
5
- VERSION = "0.3.0"
5
+ VERSION = "0.4.0"
6
6
  end
7
7
  end
@@ -278,6 +278,29 @@ The catalog offers these Keystone UI components by default: `button`,
278
278
  an Archive button, and a published page also links to View live and, when
279
279
  Chrome or Chromium is found, to its PDF. Show archived lists archived pages
280
280
  instead, each with a Restore button.
281
+ - **A page's title** is set in the builder's settings panel when nothing is
282
+ selected, and reaches visitors when the page is published. The engine's
283
+ public address sets it with `content_for(:title)`, so a host layout that
284
+ shows `content_for(:title)` in its `<title>` gets it. A host drawing a page
285
+ in its own view reads `page.live_version&.title`, which is nil when the page
286
+ has none.
287
+ - **A paragraph's text is rich text.** Editors type it in the builder and
288
+ link selected words with the Link button, and pasted text keeps only its
289
+ words and links. It is stored as `{ "html" => "..." }` and cleaned each time
290
+ it is drawn, keeping paragraphs, line breaks and links with safe addresses.
291
+ A link to an `http` or `https` address opens in a new tab. A paragraph saved
292
+ earlier as a plain string still draws as escaped text.
293
+ - **A feature grid's icons** are typed per feature in the builder, usually as
294
+ an emoji. A typed icon is drawn as text, so markup or a script in it is shown
295
+ and never run. A feature left without an icon shows the check mark.
296
+ - **A section's background and width** are set in the builder's layout panel.
297
+ A shaded section fills the full width of the page with keystone_ui's
298
+ `--ks-color-fill-muted` colour, and its dark value in a dark theme. A section
299
+ with a width of `sm`, `md`, `lg` or `xl` holds its rows at keystone_ui's
300
+ page width of that name with its page side padding, padded above and below
301
+ from `--ks-spacing`. The page builder has no setting of its own for these:
302
+ a host changes them through its keystone_ui theme values. A section saved
303
+ without either draws as it did before.
281
304
  - **A page's settings** at `/pages/manage/pages/:id/edit` change its name, slug
282
305
  and place after it is created. A slug another page of the same owner uses, or
283
306
  a place the host does not name, is refused with a message and changes nothing.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: keystone_ui-pages
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tyler Schneider
@@ -90,6 +90,7 @@ files:
90
90
  - app/controllers/keystone_ui/pages/manage/rows_controller.rb
91
91
  - app/controllers/keystone_ui/pages/manage/samples_controller.rb
92
92
  - app/controllers/keystone_ui/pages/manage/sections_controller.rb
93
+ - app/controllers/keystone_ui/pages/manage/titles_controller.rb
93
94
  - app/controllers/keystone_ui/pages/manage/versions_controller.rb
94
95
  - app/controllers/keystone_ui/pages/pages_controller.rb
95
96
  - app/helpers/keystone_ui/pages/drawing_helper.rb
@@ -103,6 +104,8 @@ files:
103
104
  - app/javascript/keystone_ui_pages/ListField.jsx
104
105
  - app/javascript/keystone_ui_pages/LookChoice.jsx
105
106
  - app/javascript/keystone_ui_pages/PageEditor.jsx
107
+ - app/javascript/keystone_ui_pages/PageTitle.jsx
108
+ - app/javascript/keystone_ui_pages/RichTextField.jsx
106
109
  - app/javascript/keystone_ui_pages/SampleChoice.jsx
107
110
  - app/javascript/keystone_ui_pages/SelectionPath.jsx
108
111
  - app/javascript/keystone_ui_pages/SelectionToolbar.jsx